LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

RAGFlow代理组件技术

2025/3/19

如果要自己创建一个组件并且使用

要创建一个新的组件类 + 自定义新的组件参数

agent/component/xxxx.py 在里面继承 ComponentParamBase + ComponentBase 类 这样就可以重用其中的大部分方法和逻辑
② 新组件类需要实现 _run 方法,这个方法会定义组件的具体业务逻辑

BaiduFanyiParam →→ ComponentBase → base.py
★★★ base.py【里面的output是输出为元组(DataFrame) 
再通过序列化转换为(字典)(def as_dict(self))JSON】 ★★★

    扩展性:不同的子类可以根据具体需求实现自己的 _run 方法
           不同的业务逻辑可能需要不同的数据处理方式和计算方法
    灵活配置:kwargs 参数提供了方法调用时灵活传递参数的能力,可以根据需要在不同情况下调整行为。
    调试和监控:通过 logging.debug 和 set_output 记录输入和输出,可以在调试和生产环境中跟踪程序的行为,并及时发现问题。
    
    def run(self, history, **kwargs):
        logging.debug("{}, history: {}, kwargs: {}".format(self, json.dumps(history, ensure_ascii=False),
                                                              json.dumps(kwargs, ensure_ascii=False)))
        self._param.debug_inputs = []
        try:
            res = self._run(history, **kwargs)
            self.set_output(res)
        except Exception as e:
            self.set_output(pd.DataFrame([{"content": str(e)}]))
            raise e

        return res

    def _run(self, history, **kwargs):
        raise NotImplementedError()
【自定义组件】BaiduFanyi →→ ComponentBase → abc.py 【Python的抽象基类(类似于Java的接口)】
确保某些类在继承抽象基类时遵循特定的协议 这在设计大型系统或者框架时非常有用
★ 接口定义:定义了组件必须实现的方法 => `_run` => 组件执行其任务的地方
★ 组件命名:通过 component_name 类属性,它提供了组件的名称,这有助于在系统中识别和管理组件
★ 运行逻辑:`_run` 方法是组件的核心,它包含了组件的执行逻辑,例如如何使用参数、如何处理输入、如何调用外部API等
★ 输入/输出处理:output → base.py 用于处理组件的输出,确保所有组件的输出格式一致
------------------------------------------------------------------------------
【自定义组件参数】BaiduFanyiParam →→ ComponentParamBase → base.py 【初始化+校验】
定义了百度翻译组件所需的参数,例如 appid、secret_key、trans_type 等,它还实现了一个 check 方法,用于验证参数的有效性。
★ 参数封装:它封装了组件运行所需的所有参数,使得参数管理更加集中方便
★ 参数验证:通过 check 方法,可以确保在组件运行前所有必要的参数都被正确设置,并且是有效的
★ 可扩展性:其他组件的参数类可以继承 ComponentParamBase 并根据需要添加或修改参数
import random
# 抽象基类模块 用于定义组件的基类
from abc import ABC
# 用于发送 HTTP 请求,与 百度翻译 API 交互
import requests
# 基础组件类,于构建此组件的父类,定义组件的基本结构和参数。
from agent.component.base import ComponentBase, ComponentParamBase
# 用于生成百度翻译 API 请求签名(加密哈希)
from hashlib import md5

# 百度翻译组件的参数定义类 继承ComponentParamBase
class BaiduFanyiParam(ComponentParamBase):
    """
    Define the BaiduFanyi component parameters.
    """

    def __init__(self):
        super().__init__()
   # appid 和 secret_key 是百度翻译 API 的 身份验证 信息(需要在百度翻译开放平台获取)
        self.appid = "xxx"
        self.secret_key = "xxx"
   # translate:普通翻译   fieldtranslate:专业领域翻译
        self.trans_type = 'translate'
        self.parameters = []
   # source_lang 翻译的源语言   target_lang 翻译的目标语言
        self.source_lang = 'auto'
        self.target_lang = 'auto'
   # domain 如果使用专业领域翻译,需指定领域(如 finance 表示 金融 领域)
        self.domain = 'finance'
# 检查参数合法性
    def check(self):
    # check_empty():确保 appid 和 secret_key 不能为空。
        self.check_empty(self.appid, "BaiduFanyi APPID")
        self.check_empty(self.secret_key, "BaiduFanyi Secret Key")
# check_valid_value():确保 trans_type、source_lang、target_lang、domain 的值在合法选项之内
        self.check_valid_value(self.trans_type, "Translate type", ['translate', 'fieldtranslate'])
# 以下是列举各种语言
        self.check_valid_value(self.source_lang, "Source language",
                               ['auto', 'zh', 'en', 'yue', 'wyw', 'jp', 'kor', 'fra', 'spa', 'th', 'ara', 'ru', 'pt',
                                'de', 'it', 'el', 'nl', 'pl', 'bul', 'est', 'dan', 'fin', 'cs', 'rom', 'slo', 'swe',
                                'hu', 'cht', 'vie'])
        self.check_valid_value(self.target_lang, "Target language",
                               ['auto', 'zh', 'en', 'yue', 'wyw', 'jp', 'kor', 'fra', 'spa', 'th', 'ara', 'ru', 'pt',
                                'de', 'it', 'el', 'nl', 'pl', 'bul', 'est', 'dan', 'fin', 'cs', 'rom', 'slo', 'swe',
                                'hu', 'cht', 'vie'])
# 以下是列举不同专业领域
        self.check_valid_value(self.domain, "Translate field",
                               ['it', 'finance', 'machinery', 'senimed', 'novel', 'academic', 'aerospace', 'wiki',
                                'news', 'law', 'contract'])

# 继承ComponentBase和ABC


# -------------------------------------------------------------------------------- #


class BaiduFanyi(ComponentBase, ABC):
 # 定义组件名称在系统内唯一标识该组件
    component_name = "BaiduFanyi"
    # _run()是组件的 核心执行函数,用于处理翻译请求。
# **kwargs:可以让函数更加灵活,因为它可以接受任意数量的命名参数
# 在函数内部,kwargs 是一个字典,包含了所有传递给函数的额外命名参数。
    def _run(self, history, **kwargs):
'''
self.get_input() 获取输入内容。
如果 content 存在,则用 - 连接多个内容(拼接成单个字符串)。
如果 ans 为空,直接返回 ""
'''
        ans = self.get_input()
        ans = " - ".join(ans["content"]) if "content" in ans else ""
        if not ans:
            return BaiduFanyi.be_output("")

        try:
'''
source_lang 和 target_lang:来源和目标语言。
appid:百度翻译 API 的 应用 ID。
salt:随机数(百度 API 需要此参数)。
secret_key:百度 API 的 密钥
'''
            source_lang = self._param.source_lang
            target_lang = self._param.target_lang
            appid = self._param.appid
            salt = random.randint(32768, 65536)
            secret_key = self._param.secret_key
-------------------------- ☆ 普通翻译API请求 ☆ --------------------------
            if self._param.trans_type == 'translate':
            # md5签名,防止请求被篡改
                sign = md5((appid + ans + salt + secret_key).encode('utf-8')).hexdigest()
     # 发送 HTTP POST 请求 访问 百度翻译 API
                url = 'http://api.fanyi.baidu.com/api/trans/vip/translate?' + 'q=' + ans + '&from=' + source_lang + '&to=' + target_lang + '&appid=' + appid + '&salt=' + salt + '&sign=' + sign
                headers = {"Content-Type": "application/x-www-form-urlencoded"}
            # 解析返回结果
                response = requests.post(url=url, headers=headers).json()

                if response.get('error_code'):
                    BaiduFanyi.be_output("**Error**:" + response['error_msg'])

                return BaiduFanyi.be_output(response['trans_result'][0]['dst'])
-------------------------- ★ 专业翻译API请求 ★ --------------------------
            elif self._param.trans_type == 'fieldtranslate':
                domain = self._param.domain
                sign = md5((appid + ans + salt + domain + secret_key).encode('utf-8')).hexdigest()
                url = 'http://api.fanyi.baidu.com/api/trans/vip/fieldtranslate?' + 'q=' + ans + '&from=' + source_lang + '&to=' + target_lang + '&appid=' + appid + '&salt=' + salt + '&domain=' + domain + '&sign=' + sign
                headers = {"Content-Type": "application/x-www-form-urlencoded"}
                response = requests.post(url=url, headers=headers).json()

                if response.get('error_code'):
                    BaiduFanyi.be_output("**Error**:" + response['error_msg'])

                return BaiduFanyi.be_output(response['trans_result'][0]['dst'])
        # 捕获 所有异常,防止程序崩溃
        except Exception as e:
            BaiduFanyi.be_output("**Error**:" + str(e))
针对于自定义组件参数[ComponentParamBase] 为什么里面是用的Json格式?
  • 方便序列化json.dumps()将对象转换为JSON字符串,可以轻松地保存或传输对象数据。

    base.py
        def __str__(self):
            """
            {
                "component_name": "Begin",
                "params": {}
            }
            """
            # .format()将变量插入到字符串模板中的占位符位置  {{}}转义{}
            return """{{
                  "component_name": "{}",
                  "params": {},
                  "output": {},
                  "inputs": {}
              }}""".format(self.component_name,
                         self._param,
    # json.dumps(...): 将获取的 "output" 和 "inputs" 值序列化为 JSON 格式的字符串
    # json.loads(str(self._param)): 将 self._param 转换为字符串,然后将其解析为 JSON 对象
    # self._param 转换为字符串,然后将其解析为 JSON 对象
    # 从 JSON 对象中获取 "output" 键对应的值,如果不存在则返回空字典 {}
                         json.dumps(json.loads(str(self._param)).get("output", {}), ensure_ascii=False),
                         json.dumps(json.loads(str(self._param)).get("inputs", []), ensure_ascii=False)
            )
    
  • 方便调试和展示:在 __str__ 方法中返回 JSON 字符串,可以方便地查看对象的内容。尤其是在调试过程中,直接打印出对象的 JSON 格式可以帮助开发人员快速查看对象的状态及其内部数据结构

    # 将对象转换为 JSON 字符串
        def __str__(self):
            return json.dumps(self.as_dict(), ensure_ascii=False)
    # 递归地将对象的属性转换成字典 适用于将对象序列化为 JSON 或进行其他操作
        def as_dict(self):
            def _recursive_convert_obj_to_dict(obj):
                ret_dict = {}
                for attr_name in list(obj.__dict__):
                    if attr_name in [_FEEDED_DEPRECATED_PARAMS, _DEPRECATED_PARAMS, _USER_FEEDED_PARAMS, _IS_RAW_CONF]:
                        continue
                    # get attr
                    attr = getattr(obj, attr_name)
                    if isinstance(attr, pd.DataFrame):
                        ret_dict[attr_name] = attr.to_dict()
                        continue
                    if attr and type(attr).__name__ not in dir(builtins):
                        ret_dict[attr_name] = _recursive_convert_obj_to_dict(attr)
                    else:
                        ret_dict[attr_name] = attr
                return ret_dict
            return _recursive_convert_obj_to_dict(self)
    
  • 与大模型交互:当涉及到与大语言模型(如 GPT)或其他机器学习模型的交互时,JSON 格式的数据通常是标准的输入和输出格式。将对象转为 JSON 字符串,能够更容易地将数据传递给模型进行处理或分析,模型通常会接受 JSON 格式的数据进行训练或推理。

如何去运用?
  • 数据交换:可以将这个 JSON 字符串用作 API 请求或响应的数据格式。例如,你可以将这个对象作为 HTTP 请求的 body 发送,或者从网络中获取 JSON 格式的数据,然后解析回对象。
  • 持久化存储:如果你需要将对象数据持久化到数据库或文件系统,JSON 是一个很好的存储格式。例如,将对象数据存储在文件中或数据库表的 JSON 类型字段中,便于未来读取和操作。
  • 配置和参数更新update 方法中接收到的 conf 参数实际上是一个字典对象,它通过递归的方式更新对象的属性。如果你将对象序列化为 JSON 格式后,可以将 JSON 作为配置文件传递给应用,应用根据该配置动态调整其行为。这使得系统更加灵活和可配置。
  • 验证与校验validate 方法利用存储在 JSON 文件中的规则对对象进行参数验证,这是一种常见的方式来确保输入的数据符合特定的格式或限制。可以通过动态加载配置文件来验证对象的数据,保证数据的合法性。
数据输出(元组)及转换(JSON)
----------------------------------- 输出为元组 -----------------------------------    
    def output(self, allow_partial=True) -> Tuple[str, Union[pd.DataFrame, partial]]:
        o = getattr(self._param, self._param.output_var_name)
        if not isinstance(o, partial):
            if not isinstance(o, pd.DataFrame):
                if isinstance(o, list):
                    return self._param.output_var_name, pd.DataFrame(o)
                if o is None:
                    return self._param.output_var_name, pd.DataFrame()
                return self._param.output_var_name, pd.DataFrame([{"content": str(o)}])
            return self._param.output_var_name, o

        if allow_partial or not isinstance(o, partial):
            if not isinstance(o, partial) and not isinstance(o, pd.DataFrame):
                return pd.DataFrame(o if isinstance(o, list) else [o])
            return self._param.output_var_name, o

        outs = None
        for oo in o():
            if not isinstance(oo, pd.DataFrame):
                # 最终返回的是一个元组
                outs = pd.DataFrame(oo if isinstance(oo, list) else [oo])
            else:
                outs = oo
        return self._param.output_var_name, outs

----------------------------------- 序列化为JSON -----------------------------------
 # 将对象转换为 JSON 字符串
    def __str__(self):
        return json.dumps(self.as_dict(), ensure_ascii=False)

    # 递归地将对象的属性转换成字典 适用于将对象序列化为 JSON 或进行其他操作
    def as_dict(self):
        def _recursive_convert_obj_to_dict(obj):
            ret_dict = {}
            for attr_name in list(obj.__dict__):
                if attr_name in [_FEEDED_DEPRECATED_PARAMS, _DEPRECATED_PARAMS,
                                 _USER_FEEDED_PARAMS, _IS_RAW_CONF]:
                    continue
                # get attr
                attr = getattr(obj, attr_name)
                if isinstance(attr, pd.DataFrame):
                    ret_dict[attr_name] = attr.to_dict()
                    continue
                if attr and type(attr).__name__ not in dir(builtins):
                    ret_dict[attr_name] = _recursive_convert_obj_to_dict(attr)
                else:
                    ret_dict[attr_name] = attr

            return ret_dict

        return _recursive_convert_obj_to_dict(self)
    
--------------------------------------------------------------------------------------
def get_stream_input(self):
def get_input_elements(self):
    
def get_input(self):
# 上游组件的输出:它会检查当前组件的上游组件,并尝试从这些组件获取输出数据作为当前的输入
# 如果 _param.query 存在,它会遍历查询参数,并根据参数的类型(如组件 ID)来决定如何获取相应的输入数据
# input后再通过DataFrame转换为元组 再去转换为字典
# 来回来转换是为了满足特定的接口要求或者为了确保数据在不同层之间传输时的兼容性



'''
                           使用Pandas库来处理数据
★ get_input函数: 这个函数首先检查是否有调试输入,如果有,则返回一个包含调试输入内容的DataFrame。然后,它处理查询输入,根据查询中的不同条件(如component_id、value等),调用其他函数获取相应的输入内容,并将这些内容添加到self._param.inputs列表中。最后,它通过pd.concat函数将所有上游组件[结合history]的输出合并成一个DataFrame,并返回。

★ get_input_elements函数: 这个函数根据self._param.query中的查询,构建一个输入元素列表,其中每个元素包含名称、键和值。

★get_stream_input函数: 这个函数处理流式输入,根据组件路径,调用其他函数获取相应的输入内容,并返回。

主要是处理输入参数,并根据不同的条件获取相应的输入内容。它并没有直接接受不同种类类型的入参,而是根据传入的查询参数来获取输入内容。
'''

agent/component/baidufanyi.py 【后端代码】
agent/component/_ _ init _ _.py 【初始化】
③ web/src/pages/flow/flow-drawer/index.tsx【表单抽屉组件 UI展示】
④ web/src/pages/flow/form/baidu-fanyi-form/index.tsx 【提供用户界面让用户能配置并提交参数】
⑤ web/src/pages/flow/constant.tsx 【定义一个应用程序中使用的各种图标、常量、枚举、接口、状态和函数】
⑥ web/src/locales/zh.ts 【把所有要显示的搞到zh.ts中(前端组件描述)】
⑦ web/src/pages/agent/constant.tsx【引入组件所需的各种图标、以及操作项的样式、初始值、语言等】
⑧ web/src/pages/agent/form-sheet/use-form-config-map.tsx 【表单配置映射、配置不同操作对应表单组件】
⑨ web/src/pages/agent/form/baidu-fanyi-form/index.tsx 【构建配置表单 】
⑩ web/src/pages/agent/hooks.tsx 【管理图形界面流程图 各种钩子 → 建数据处理流程】





创建动态代理组件 [实战]

📌 实现步骤

  1. 后端(Python FastAPI)
    • 创建一个 API 服务器,提供组件注册、查询、运行接口。
    • 组件信息存储在内存(可扩展到数据库)。
    • 组件代码动态执行,支持 exec() 加载。
  2. 前端(HTML + JavaScript)
    • 提供一个表单,允许用户输入组件名称参数Python 代码
    • 提交表单后,通过 fetch() 发送 POST 请求给后端。
    • 组件创建成功后,前端可以调用 run 接口测试组件。
  3. Postman 测试
    • 先创建组件 POST /api/components/create
    • 再运行组件 POST /api/components/run

1️⃣ 后端代码(FastAPI)

创建 server.py,这个文件用于启动 FastAPI 服务器,并提供 API 端点。

安装依赖(如果未安装 FastAPI 和 Uvicorn):

pip install fastapi uvicorn pandas

📌 代码(server.py):

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import pandas as pd

app = FastAPI()

# 组件存储(模拟数据库)
components = {}

class ComponentData(BaseModel):
    name: str
    params: dict
    code: str

@app.post("/api/components/create")
def create_component(component: ComponentData):
    """
    创建新的动态组件,存储在内存中,并使用 exec() 加载组件代码
    """
    if component.name in components:
        raise HTTPException(status_code=400, detail="组件已存在")
    
    # 安全地执行代码,存储类定义
    local_vars = {}
    exec(component.code, globals(), local_vars)
    
    if component.name not in local_vars:
        raise HTTPException(status_code=400, detail="代码中必须定义同名类")
    
    components[component.name] = {
        "params": component.params,
        "class": local_vars[component.name]
    }
    return {"message": f"组件 {component.name} 创建成功"}

@app.get("/api/components")
def list_components():
    """
    获取所有注册的组件
    """
    return {"components": list(components.keys())}

class RunRequest(BaseModel):
    component_name: str
    history: list
    params: dict

@app.post("/api/components/run")
def run_component(request: RunRequest):
    """
    运行指定的组件,并返回其输出
    """
    if request.component_name not in components:
        raise HTTPException(status_code=404, detail="组件不存在")

    component_class = components[request.component_name]["class"]
    component_instance = component_class()
    
    # 调用 _run 方法,模拟运行
    try:
        result = component_instance._run(request.history, **request.params)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"组件执行错误: {str(e)}")
    
    return {"result": result.to_dict(orient="records")}

# 运行服务器(可选:手动执行 uvicorn server:app --reload)
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

2️⃣ 前端代码

📌 HTML 页面

📌 代码(index.html):

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态组件管理</title>
</head>
<body>
    <h2>创建动态组件</h2>
    <form id="componentForm">
        <label>组件名称:</label>
        <input type="text" id="componentName" required><br><br>

        <label>参数 (JSON 格式):</label>
        <textarea id="componentParams" required>{"param1": "value1"}</textarea><br><br>

        <label>Python 代码:</label>
        <textarea id="componentCode" required>
class TestComponent:
    def _run(self, history, **kwargs):
        return pd.DataFrame([{'content': 'Hello, this is a test!'}])
        </textarea><br><br>

        <button type="button" onclick="submitNewComponent()">提交组件</button>
    </form>

    <h2>运行组件</h2>
    <label>组件名称:</label>
    <input type="text" id="runComponentName" required><br><br>
    <button type="button" onclick="runComponent()">运行组件</button>

    <h3>运行结果:</h3>
    <pre id="result"></pre>

    <script src="test.js"></script>
</body>
</html>

📌 JavaScript 代码

📌 代码(test.js):

const apiBaseUrl = "http://localhost:8000/api";  // 替换为你的服务器地址

function submitNewComponent() {
    const name = document.getElementById("componentName").value;
    const params = document.getElementById("componentParams").value;
    const code = document.getElementById("componentCode").value;

    fetch(`${apiBaseUrl}/components/create`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            name: name,
            params: JSON.parse(params),
            code: code
        })
    })
    .then(response => response.json())
    .then(data => alert(data.message || JSON.stringify(data)))
    .catch(error => console.error("Error:", error));
}

function runComponent() {
    const componentName = document.getElementById("runComponentName").value;

    fetch(`${apiBaseUrl}/components/run`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            component_name: componentName,
            history: [],
            params: {}
        })
    })
    .then(response => response.json())
    .then(data => {
        document.getElementById("result").innerText = JSON.stringify(data, null, 2);
    })
    .catch(error => console.error("Error:", error));
}

3️⃣ 使用 Postman 测试

启动服务器后,可以使用 Postman 测试 API。

(1)创建动态组件

  • 方法POST

  • URLhttp://localhost:8000/api/components/create

  • Body(JSON)

    {
      "name": "TestComponent",
      "params": {
        "param1": "value1"
      },
      "code": "class TestComponent:\n    def _run(self, history, **kwargs):\n        return pd.DataFrame([{'content': 'Hello, world!'}])"
    }
    
  • 期望结果

    {
      "message": "组件 TestComponent 创建成功"
    }
    

(2)运行组件

  • 方法POST

  • URLhttp://localhost:8000/api/components/run

  • Body(JSON)

    {
      "component_name": "TestComponent",
      "history": [],
      "params": {}
    }
    
  • 期望结果

    {
      "result": [{"content": "Hello, world!"}]
    }
    

总结

  • 后端:FastAPI 实现了组件创建、查询、运行的 API。
  • 前端:HTML + JS 提供交互界面,用户可输入参数和代码。
  • 测试:可用 Postman 调试,确保组件能被动态创建和运行。

🚀 现在,你可以在 Postman 或前端界面中测试你的动态组件功能了!


纯后端情况如何添加自定义组件呢

这里以我琢磨的JSONPath自定义组件为例

首先要清楚RAGFlow的执行流程 要了解画布的流程 推荐先看一遍RAGFlow执行流程的文档 然后了解一下canvas.py和canvas_app.py的源码 掌握整体流程。

这里最重要的一个思想要明白 现在你只有后端 你没有前端 你在RAGFlow图形化界面拖拽进去的形式已经无法针对于纯后端添加自定义组件了 那怎么办?我们来梳理一下思路 我们想要什么?我们想要图形化界面拖拽过后的整体数据 也就是数据库里的dsl,我们要拿到它 然后用我们自己开发的自定义组件去解析这个大大的JSON。那么这里就涉及到没有前端 你如何去后端去添加自己做的自定义组件!当然常规方法 去图形化界面拖拽已经无法完成这种操作 你只能把希望寄托在数据库

我们来打开数据库 此时你需要一个组件的数据表 component

– rag_flow.component definition

CREATE TABLE component (

id varchar(32) NOT NULL,

create_time bigint DEFAULT NULL,

create_date datetime DEFAULT NULL,

update_time bigint DEFAULT NULL,

update_date datetime DEFAULT NULL,

tenant_id varchar(32) NOT NULL,

module varchar(255) NOT NULL,

created_by varchar(32) NOT NULL,

is_deleted tinyint DEFAULT ‘0’,

PRIMARY KEY (id)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

这是创建表的信息 目前我们只需要关注module里面的 我们要把我们自己开发的组件按照那些形式添加进去 我的组件叫:”jsonpath” 那么我的module里面就应该是agent.component.jsonpath 当然你要按照它的风格把组件放在这个包里面。

其次还要注意在这个包里面的__init__.py

把你的组件加进去 “jsonpath”:”JSONPath”

此时并非万事大吉 因为我们目前只有纯后端 没有前端那些 所以回到开头所说 你要自己把你的组件以json的格式手动加入到数据库的dsl中 因为很长所以你需要JSON在线解析格式化验证 - JSON.cn去让它展示的更直观

这是我组件需要的json 你要手动的去添加到dsl 步骤就是先复制数据库的dsl 然后放在解析器里面 手动的去添加自己的组件

“JSONPath:PathProcessor”: {“obj”: {“component_name”: “JSONPath”,”params”: {“output_var_name”: “output”,”jsonpath_expr”: “$.components[*].obj.component_name”,”debug_mode”: true,”query”: [],”inputs”: [],”debug_inputs”: [],”output”: {“content”: {“0”: “”}}}},”downstream”: [“Generate:TwelveClocksSpeak”],”upstream”: []}

这只是我的组件json 你需要在完整的dsl把它插入进去 按照它们形成的格式 这里说明一下 dsl就是你在图形化界面创建agent的时候里面各各组件的相关信息 当然你还要注意自己写的组件类型 是用来干什么的 你才能去做到对应相应的处理 比如说我这个是解析json的 那我就不能去拿上下文组件的输入或输出 正常的流程是连线后你要拿到上游的输出结果 作为你组件的输入结果。我这个则是拿到全局画布canvas_data数据再去进行解析

所以要明白需求才能去更好的融入代码中 经过这几天的学习真的感觉 只有自己一点点的摸索过后才能真正的印象深刻 才能慢慢变得强大!

阅读全文

RAGFlow工具技术文档

2025/3/7

RAGFlow代理组件的核心

  • 集成外部数据源:通过检索模块来继承外部知识库、数据库、文档等内容,增强模型的回答能力
  • 生成式任务处理:利用模型生成的能力,基于用户的查询返回自然语言的回答
  • 模块化:RAGFlow支持灵活的模块化设计,可以通过配置和集成不同的组件来满足特定的需求

RAGFlow代理组件

RAGFlow 的代理组件是其架构中的核心部分,负责管理与外部数据源生成模型的交互。代理组件不仅可以从不同的外部数据源中获取知识,还能将检索到的信息与生成模型结合,从而生成更加准确的回答。

代理组件的功能

  • 数据检索:代理组件会从多个外部数据源(如数据库、文档、API等)进行数据检索,获取与用户查询相关的信息。
  • 生成模型的调用:将检索到的信息与生成模型结合,通过生成模型提供一个流畅且准确的答案。
  • 多源数据融合:在生成回答时,代理组件能够处理多来源的数据,并将其融合成最终的答案。

组件 → web/src/utils *待定

组件前端交互中心web/src/utils/api.ts
它定义了一些与“画布”相关的 API 接口地址,用于与后端进行通信。通过这些接口,前端应用可以进行不同操作,如获取模板、操作画布、调试等

listTemplates: 用于获取所有画布模板的列表,发送请求到 /canvas/templates
listCanvas: 获取画布列表的接口,发送请求到 /canvas/list

Begin组件

Begin 组件设置开场问候语或接受用户的输入。当您创建代理时,它会自动填充到画布上,无论是从模板还是从头开始(从空白模板)。工作流程中应该只有一个 Begin 组件。

api/ragflow/web/src/locales/zh.ts
……
setAnOpenerInitial: 你好! 我是你的助理,有什么可以帮到你的吗?,

静态消息: /v1/canvas/set
生成问答、问题优化、问题类型、关键词/v1/llm/list
知识检索/v1/llm/list + /v1/kb/list

以**问题优化 **(RewriteQuestion:MoodyCrabsPeel) 为例:


后端内容

知识检索Retrieval 路径→agent/component/retrieval.py

# 记录日志
import logging
# 抽象基类模块
from abc import ABC
# 用于数据处理,尤其是处理数据框(DataFrame)
import pandas as pd
# 从数据库相关模块导入 LLMType,这个可能是用来定义 LLM(语言模型)类型的枚举
from api.db import LLMType
# 从数据库服务模块中导入,分别用于知识库服务和 LLM 配置的封装
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.llm_service import LLMBundle
# 加载应用程序的设置
from api import settings
# 基础组件类,用于构建此组件的父类
from agent.component.base import ComponentBase, ComponentParamBase
# 从标签模块中导入,用于对查询进行标记
from rag.app.tag import label_question


class RetrievalParam(ComponentParamBase):

    """
    Define the Retrieval component parameters.
    """
    def __init__(self):
        super().__init__()
        # 相似度阈值
        self.similarity_threshold = 0.2
        # 关键字相似度的权重
        self.keywords_similarity_weight = 0.5
        # 检索时返回的结果数
        self.top_n = 8
        # 在检索时考虑的最大数量
        self.top_k = 1024
        # 知识库的 ID 列表
        self.kb_ids = []
        # 重新排序模型的 ID (可能为空)
        self.rerank_id = ""
        # 当没有找到任何结果时的响应文本
        self.empty_response = ""
#### check_decimal_float???
   # check():对参数进行验证,确保相似度阈值和权重为有效的小数,且 top_n 是正数。
    def check(self):
        # "相似性阈值"
        self.check_decimal_float(self.similarity_threshold, "[Retrieval] Similarity threshold")
        # "关键词相似度权重"
        self.check_decimal_float(self.keywords_similarity_weight, "[Retrieval] Keyword similarity weight")
        self.check_positive_number(self.top_n, "[Retrieval] Top N")

# Retrieval 类继承自 ComponentBase 和 ABC(抽象基类)
# 表示这是一个组件,并实现了抽象方法
class Retrieval(ComponentBase, ABC):
    component_name = "Retrieval"

##### history, **kwargs???
    # _run为核心方法 
# query输入查询内容 包括'content'键 提取第一个内容项 将查询内容转换为字符串
    def _run(self, history, **kwargs):
        query = self.get_input()
        query = str(query["content"][0]) if "content" in query else ""
    # 从知识库服务中获取指定 ID 的知识库。如果没有找到相关的知识库,返回空的响应。
        kbs = KnowledgebaseService.get_by_ids(self._param.kb_ids)
        if not kbs:
            return Retrieval.be_output("")
# 获取所有知识库的嵌入模型 ID(embd_id),确保所有知识库使用相同的嵌入模型。
# 如果使用的嵌入模型不一致,抛出异常
        embd_nms = list(set([kb.embd_id for kb in kbs]))
        assert len(embd_nms) == 1, "Knowledge bases use different embedding models."

##### LLMBundle???
# 创建 LLMBundle 实例来封装嵌入模型,使用画布的租户 ID 和嵌入模型的 ID 配置嵌入模型,并设置到画布
        embd_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.EMBEDDING, embd_nms[0])
        self._canvas.set_embedding_model(embd_nms[0])

   # 若提供了 rerank_id,则加载重新排序的模型
        rerank_mdl = None
        if self._param.rerank_id:
            rerank_mdl = LLMBundle(kbs[0].tenant_id, LLMType.RERANK, self._param.rerank_id)

  # 使用 settings.retrievaler.retrieval 方法执行知识检索操作,传入查询和各种配置参数
# query:用户查询   embd_mdl:嵌入模型    其他参数包括知识库 ID、相似度阈值、关键词相似度权重等
        kbinfos = settings.retrievaler.retrieval(query, embd_mdl, kbs[0].tenant_id, self._param.kb_ids,
                                        1, self._param.top_n,
                                        self._param.similarity_threshold, 1 - self._param.keywords_similarity_weight,
                                        aggs=False, rerank_mdl=rerank_mdl,
                                        rank_feature=label_question(query, kbs))
# 如果检索结果为空(没有找到相关内容),则返回空响应
# 如果设置了 empty_response,则在响应中返回该内容。
        if not kbinfos["chunks"]:
            df = Retrieval.be_output("")
            if self._param.empty_response and self._param.empty_response.strip():
                df["empty_response"] = self._param.empty_response
            return df
'''
如果有检索结果,将结果转换为 Pandas 的 DataFrame 格式。
将 content_with_weight 重命名为 content,然后删除 content_with_weight 列。
输出调试日志,记录查询和检索结果。
'''
        df = pd.DataFrame(kbinfos["chunks"])
        df["content"] = df["content_with_weight"]
        del df["content_with_weight"]
        logging.debug("{} {}".format(query, df))
        return df

如果你需要一个 定制的代理组件,我可以基于这个 Retrieval 组件的架构,为你设计一个类似的组件,满足你的特定需求。

你可以告诉我:

  1. 代理组件的具体用途(比如:请求转发、智能路由、流量控制、负载均衡、身份验证等)。
  2. 需要支持的参数(比如:代理地址、超时时间、认证方式等)。
  3. 核心功能(比如:日志记录、请求缓存、故障恢复等)。
  4. 集成的其他服务(比如:Redis、RabbitMQ、数据库等)。

如果你希望它遵循 ComponentBase 组件框架,我可以按照该模式来实现,让它可以与现有系统无缝对接!

下面的是引用的重要方法上一个代码块里的

retrieval.pybase.py

**kwargs 允许调用 _run() 方法时 传入任意数量的额外参数,即使这些参数没有在方法签名中显式列出#上方代码索引:#### check_decimal_float???
# check():对参数进行验证,确保【相似度阈值】和【权重】为有效的小数,且 top_n 是正数。
    def check(self):
        # "相似性阈值"
        self.check_decimal_float(self.similarity_threshold, "[Retrieval] Similarity threshold")
        # "关键词相似度权重"
        self.check_decimal_float(self.keywords_similarity_weight, "[Retrieval] Keyword similarity weight")
        self.check_positive_number(self.top_n, "[Retrieval] Top N")
---------------------------  ☆ 上方代码块片段 ☆  ---------------------------

# 校验检索相关参数的合法性
# 这是一个 静态方法,用于校验参数 param 是否是 0 到 1 之间的浮点数或整数

'''
校验逻辑:
param 必须是 float 或 int 类型(不允许 str、list 等)。
param 需要在 [0, 1] 之间,否则抛出 ValueError 异常,并在错误信息中包含 descr 描述。
'''
# 这里为什么是0-1 不能超过1? 如果 similarity_threshold = 1.2,会触发 check_decimal_float 的异常抛出,提示超出 [0,1] 范围。而且在前端的拖拉中也只会是0~1之间。因为这些参数代表 归一化(normalized)后的比例或权重,它们的值通常不能超过 1
# 【0表示示完全相同】
    @staticmethod
    def check_decimal_float(param, descr):
        if type(param).__name__ not in ["float", "int"] or param < 0 or param > 1:
            raise ValueError(
                descr
                + " {} not supported, should be a float number in range [0, 1]".format(
                    param
                )
            )
'''
这里为什么要用 @staticmethod?
1. 避免创建对象,提高效率 👉 由于静态方法不依赖实体属性或方法,因此可以直接通过类调用,而不必创建对象
2. 代码组织清晰 👉 适用于逻辑独立的方法,可以让代码更容易读、更结构化
3. 防止修改实例状态 👉 由于静态方法不能访问self,因此不会修改实例的状态,保证了方法的纯函数特征
-------- -------- -------- 结合代码解析 -------- -------- -------- 
4. 这个方法 不访问实例变量,它只 检查参数 是否符合 0~1 之间的范围
5. 适用于 工具方法,属于 数据验证逻辑,不依赖 self
6. 可以直接通过类调用:
Retrieval.check_decimal_float(0.8, "Threshold")  # 正常
Retrieval.check_decimal_float(1.2, "Threshold")  # 抛出 ValueError
'''
==========================================================================
#上方代码索引:##### history, **kwargs???
    def _run(self, history, **kwargs):
        query = self.get_input()
        query = str(query["content"][0]) if "content" in query else ""
        ......
# 讲解重点:【**kwargs】
# [**kwargs] 允许调用 _run() 方法时 传入任意数量的额外参数,即使这些参数没有在方法签名中显式列出 比如:👇
    _run(self, history, user_id=123, debug=True, mode="fast")
# 在 _run() 方法内部,这些参数可以通过 kwargs["user_id"]、kwargs["debug"] 访问,但 当前代码里没有使用 kwargs,所以它只是用来兼容可能的扩展。
'''
目前 **kwargs 没有被使用,它的作用是:

扩展性:可以传递额外的参数而不修改函数签名
兼容性:允许不同调用方传递不同的参数,而 _run() 仍能正常运行
适用场景:未来需要额外参数
目前 **kwargs 只是 占位符,但它提供了未来扩展的可能性
'''
特征 普通方法(self) 静态方法(@staticmethod) 类方法(@classmethod)
依赖实例(self) × ×
依赖类(cls) × ×
访问实例变量 × ×
访问类变量 ×
适用于 需要访问实例 工具方法、验证方法 需要修改类变量

retrieval.py




让我们再来看一个案例

百度翻译baidufanyi.py 路径 → agent/component/baidufanyi.py

# 
import random
# 抽象基类模块 用于定义组件的基类
from abc import ABC
# 用于发送 HTTP 请求,与 百度翻译 API 交互
import requests
# 基础组件类,于构建此组件的父类,定义组件的基本结构和参数。
from agent.component.base import ComponentBase, ComponentParamBase
# 用于生成百度翻译 API 请求签名(加密哈希)
from hashlib import md5

# 百度翻译组件的参数定义类 继承ComponentParamBase
class BaiduFanyiParam(ComponentParamBase):
    """
    Define the BaiduFanyi component parameters.
    """

    def __init__(self):
        super().__init__()
   # appid 和 secret_key 是百度翻译 API 的 身份验证 信息(需要在百度翻译开放平台获取)
        self.appid = "xxx"
        self.secret_key = "xxx"
   # translate:普通翻译   fieldtranslate:专业领域翻译
        self.trans_type = 'translate'
        self.parameters = []
   # source_lang 翻译的源语言   target_lang 翻译的目标语言
        self.source_lang = 'auto'
        self.target_lang = 'auto'
   # domain 如果使用专业领域翻译,需指定领域(如 finance 表示 金融 领域)
        self.domain = 'finance'
# 检查参数合法性
    def check(self):
    # check_empty():确保 appid 和 secret_key 不能为空。
        self.check_empty(self.appid, "BaiduFanyi APPID")
        self.check_empty(self.secret_key, "BaiduFanyi Secret Key")
# check_valid_value():确保 trans_type、source_lang、target_lang、domain 的值在合法选项之内
        self.check_valid_value(self.trans_type, "Translate type", ['translate', 'fieldtranslate'])
# 以下是列举各种语言
        self.check_valid_value(self.source_lang, "Source language",
                               ['auto', 'zh', 'en', 'yue', 'wyw', 'jp', 'kor', 'fra', 'spa', 'th', 'ara', 'ru', 'pt',
                                'de', 'it', 'el', 'nl', 'pl', 'bul', 'est', 'dan', 'fin', 'cs', 'rom', 'slo', 'swe',
                                'hu', 'cht', 'vie'])
        self.check_valid_value(self.target_lang, "Target language",
                               ['auto', 'zh', 'en', 'yue', 'wyw', 'jp', 'kor', 'fra', 'spa', 'th', 'ara', 'ru', 'pt',
                                'de', 'it', 'el', 'nl', 'pl', 'bul', 'est', 'dan', 'fin', 'cs', 'rom', 'slo', 'swe',
                                'hu', 'cht', 'vie'])
# 以下是列举不同专业领域
        self.check_valid_value(self.domain, "Translate field",
                               ['it', 'finance', 'machinery', 'senimed', 'novel', 'academic', 'aerospace', 'wiki',
                                'news', 'law', 'contract'])

# 继承ComponentBase和ABC
class BaiduFanyi(ComponentBase, ABC):
 # 定义组件名称在系统内唯一标识该组件
    component_name = "BaiduFanyi"
    # _run()是组件的 核心执行函数,用于处理翻译请求。
# **kwargs:可以让函数更加灵活,因为它可以接受任意数量的命名参数
# 在函数内部,kwargs 是一个字典,包含了所有传递给函数的额外命名参数。
    def _run(self, history, **kwargs):
'''
self.get_input() 获取输入内容。
如果 content 存在,则用 - 连接多个内容(拼接成单个字符串)。
如果 ans 为空,直接返回 ""
'''
        ans = self.get_input()
        ans = " - ".join(ans["content"]) if "content" in ans else ""
        if not ans:
            return BaiduFanyi.be_output("")

        try:
'''
source_lang 和 target_lang:来源和目标语言。
appid:百度翻译 API 的 应用 ID。
salt:随机数(百度 API 需要此参数)。
secret_key:百度 API 的 密钥
'''
            source_lang = self._param.source_lang
            target_lang = self._param.target_lang
            appid = self._param.appid
            salt = random.randint(32768, 65536)
            secret_key = self._param.secret_key
-------------------------- ☆ 普通翻译API请求 ☆ --------------------------
            if self._param.trans_type == 'translate':
            # md5签名,防止请求被篡改
                sign = md5((appid + ans + salt + secret_key).encode('utf-8')).hexdigest()
     # 发送 HTTP POST 请求 访问 百度翻译 API
                url = 'http://api.fanyi.baidu.com/api/trans/vip/translate?' + 'q=' + ans + '&from=' + source_lang + '&to=' + target_lang + '&appid=' + appid + '&salt=' + salt + '&sign=' + sign
                headers = {"Content-Type": "application/x-www-form-urlencoded"}
            # 解析返回结果
                response = requests.post(url=url, headers=headers).json()

                if response.get('error_code'):
                    BaiduFanyi.be_output("**Error**:" + response['error_msg'])

                return BaiduFanyi.be_output(response['trans_result'][0]['dst'])
-------------------------- ★ 专业翻译API请求 ★ --------------------------
            elif self._param.trans_type == 'fieldtranslate':
                domain = self._param.domain
                sign = md5((appid + ans + salt + domain + secret_key).encode('utf-8')).hexdigest()
                url = 'http://api.fanyi.baidu.com/api/trans/vip/fieldtranslate?' + 'q=' + ans + '&from=' + source_lang + '&to=' + target_lang + '&appid=' + appid + '&salt=' + salt + '&domain=' + domain + '&sign=' + sign
                headers = {"Content-Type": "application/x-www-form-urlencoded"}
                response = requests.post(url=url, headers=headers).json()

                if response.get('error_code'):
                    BaiduFanyi.be_output("**Error**:" + response['error_msg'])

                return BaiduFanyi.be_output(response['trans_result'][0]['dst'])
        # 捕获 所有异常,防止程序崩溃
        except Exception as e:
            BaiduFanyi.be_output("**Error**:" + str(e))

深层次研究

from agent.component.base import ComponentBase, ComponentParamBase

主要用于处理、更新、验证和警告组件参数,适用于那些需要动态配置、递归嵌套结构且要求高可扩展性的系统

class ComponentParamBase(ABC):
    def __init__(self):
        # 初始化输出变量名
        self.output_var_name = "output"
        # 初始化消息历史窗口大小
        self.message_history_window_size = 22
        # 初始化查询参数
        self.query = []
        # 初始化输入参数
        self.inputs = []
        # 初始化调试输入参数
        self.debug_inputs = []
    # 设置组件的名称,并返回当前对象,支持链式调用
    def set_name(self, name: str):
        self._name = name
        return self

    # 抽象方法 子类必须实现 它的目的是检查参数是否有效
    def check(self):
        raise NotImplementedError("Parameter Object should be checked.")

    # 类方法 用于检查类是否具有某个属性 _DEPRECATED_PARAMS 如果没有 则初始化一个空的集合
    @classmethod
    def _get_or_init_deprecated_params_set(cls):
        if not hasattr(cls, _DEPRECATED_PARAMS):
            setattr(cls, _DEPRECATED_PARAMS, set())
        return getattr(cls, _DEPRECATED_PARAMS)
    # 用于检查和初始化实例的已废弃参数集合
    def _get_or_init_feeded_deprecated_params_set(self, conf=None):
        if not hasattr(self, _FEEDED_DEPRECATED_PARAMS):
            if conf is None:
                setattr(self, _FEEDED_DEPRECATED_PARAMS, set())
            else:
                setattr(
                    self,
                    _FEEDED_DEPRECATED_PARAMS,
                    set(conf[_FEEDED_DEPRECATED_PARAMS]),
                )
        return getattr(self, _FEEDED_DEPRECATED_PARAMS)
    # 用于检查和初始化实例的已废弃参数集合
    def _get_or_init_user_feeded_params_set(self, conf=None):
        if not hasattr(self, _USER_FEEDED_PARAMS):
            if conf is None:
                setattr(self, _USER_FEEDED_PARAMS, set())
            else:
                setattr(self, _USER_FEEDED_PARAMS, set(conf[_USER_FEEDED_PARAMS]))
        return getattr(self, _USER_FEEDED_PARAMS)
    # 返回用户提供的参数
    def get_user_feeded(self):
        return self._get_or_init_user_feeded_params_set()
    # 返回已废弃的参数集合
    def get_feeded_deprecated_params(self):
        return self._get_or_init_feeded_deprecated_params_set()
    
    # @装饰器 定义了 _deprecated_params_set 属性 它返回已废弃的参数集合
    @property
    def _deprecated_params_set(self):
        return {name: True for name in self.get_feeded_deprecated_params()}
    
    # 将对象转换为 JSON 字符串
    def __str__(self):
        return json.dumps(self.as_dict(), ensure_ascii=False)

    # 递归地将对象的属性转换成字典 适用于将对象序列化为 JSON 或进行其他操作
    def as_dict(self):
        def _recursive_convert_obj_to_dict(obj):
            ret_dict = {}
            for attr_name in list(obj.__dict__):
                if attr_name in [_FEEDED_DEPRECATED_PARAMS, _DEPRECATED_PARAMS, _USER_FEEDED_PARAMS, _IS_RAW_CONF]:
                    continue
                # get attr
                attr = getattr(obj, attr_name)
                if isinstance(attr, pd.DataFrame):
                    ret_dict[attr_name] = attr.to_dict()
                    continue
                if attr and type(attr).__name__ not in dir(builtins):
                    ret_dict[attr_name] = _recursive_convert_obj_to_dict(attr)
                else:
                    ret_dict[attr_name] = attr

            return ret_dict

        return _recursive_convert_obj_to_dict(self)

    # 用于更新对象的参数,并根据传入的配置进行修改 它处理递归更新参数、检查冗余属性等
    def update(self, conf, allow_redundant=False):
        update_from_raw_conf = conf.get(_IS_RAW_CONF, True)
        if update_from_raw_conf:
            deprecated_params_set = self._get_or_init_deprecated_params_set()
            feeded_deprecated_params_set = (
                self._get_or_init_feeded_deprecated_params_set()
            )
            user_feeded_params_set = self._get_or_init_user_feeded_params_set()
            setattr(self, _IS_RAW_CONF, False)
        else:
            feeded_deprecated_params_set = (
                self._get_or_init_feeded_deprecated_params_set(conf)
            )
            user_feeded_params_set = self._get_or_init_user_feeded_params_set(conf)

        def _recursive_update_param(param, config, depth, prefix):
            if depth > settings.PARAM_MAXDEPTH:
                raise ValueError("Param define nesting too deep!!!, can not parse it")

            inst_variables = param.__dict__
            redundant_attrs = []
            for config_key, config_value in config.items():
                # redundant attr
                if config_key not in inst_variables:
                    if not update_from_raw_conf and config_key.startswith("_"):
                        setattr(param, config_key, config_value)
                    else:
                        setattr(param, config_key, config_value)
                        # redundant_attrs.append(config_key)
                    continue

                full_config_key = f"{prefix}{config_key}"

                if update_from_raw_conf:
                    # add user feeded params
                    user_feeded_params_set.add(full_config_key)

                    # update user feeded deprecated param set
                    if full_config_key in deprecated_params_set:
                        feeded_deprecated_params_set.add(full_config_key)

                # supported attr
                attr = getattr(param, config_key)
                if type(attr).__name__ in dir(builtins) or attr is None:
                    setattr(param, config_key, config_value)

                else:
                    # recursive set obj attr
                    sub_params = _recursive_update_param(
                        attr, config_value, depth + 1, prefix=f"{prefix}{config_key}."
                    )
                    setattr(param, config_key, sub_params)

            if not allow_redundant and redundant_attrs:
                raise ValueError(
                    f"cpn `{getattr(self, '_name', type(self))}` has redundant parameters: `{[redundant_attrs]}`"
                )

            return param

        return _recursive_update_param(param=self, config=conf, depth=0, prefix="")

    # 用于提取所有非内置类型的属性 递归地遍历对象的属性
    def extract_not_builtin(self):
        def _get_not_builtin_types(obj):
            ret_dict = {}
            for variable in obj.__dict__:
                attr = getattr(obj, variable)
                if attr and type(attr).__name__ not in dir(builtins):
                    ret_dict[variable] = _get_not_builtin_types(attr)

            return ret_dict

        return _get_not_builtin_types(self)
    
    # validate 方法用于验证对象的参数是否符合预定义的规则 规则存储在 JSON 文件中
    def validate(self):
        self.builtin_types = dir(builtins)
        self.func = {
            "ge": self._greater_equal_than,
            "le": self._less_equal_than,
            "in": self._in,
            "not_in": self._not_in,
            "range": self._range,
        }
        home_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
        param_validation_path_prefix = home_dir + "/param_validation/"

        param_name = type(self).__name__
        param_validation_path = "/".join(
            [param_validation_path_prefix, param_name + ".json"]
        )

        validation_json = None

        try:
            with open(param_validation_path, "r") as fin:
                validation_json = json.loads(fin.read())
        except BaseException:
            return

        self._validate_param(self, validation_json)

    def _validate_param(self, param_obj, validation_json):
        default_section = type(param_obj).__name__
        var_list = param_obj.__dict__

        for variable in var_list:
            attr = getattr(param_obj, variable)

            if type(attr).__name__ in self.builtin_types or attr is None:
                if variable not in validation_json:
                    continue

                validation_dict = validation_json[default_section][variable]
                value = getattr(param_obj, variable)
                value_legal = False

                for op_type in validation_dict:
                    if self.func[op_type](value, validation_dict[op_type]):
                        value_legal = True
                        break

                if not value_legal:
                    raise ValueError(
                        "Plase check runtime conf, {} = {} does not match user-parameter restriction".format(
                            variable, value
                        )
                    )

            elif variable in validation_json:
                self._validate_param(attr, validation_json)

    # 用于验证参数的类型和范围 确保符合预期          
    @staticmethod
    def check_string(param, descr):
        if type(param).__name__ not in ["str"]:
            raise ValueError(
                descr + " {} not supported, should be string type".format(param)
            )

    @staticmethod
    def check_empty(param, descr):
        if not param:
            raise ValueError(
                descr + " does not support empty value."
            )

    @staticmethod
    def check_positive_integer(param, descr):
        if type(param).__name__ not in ["int", "long"] or param <= 0:
            raise ValueError(
                descr + " {} not supported, should be positive integer".format(param)
            )

    @staticmethod
    def check_positive_number(param, descr):
        if type(param).__name__ not in ["float", "int", "long"] or param <= 0:
            raise ValueError(
                descr + " {} not supported, should be positive numeric".format(param)
            )

    @staticmethod
    def check_nonnegative_number(param, descr):
        if type(param).__name__ not in ["float", "int", "long"] or param < 0:
            raise ValueError(
                descr
                + " {} not supported, should be non-negative numeric".format(param)
            )

    @staticmethod
    def check_decimal_float(param, descr):
        if type(param).__name__ not in ["float", "int"] or param < 0 or param > 1:
            raise ValueError(
                descr
                + " {} not supported, should be a float number in range [0, 1]".format(
                    param
                )
            )

    @staticmethod
    def check_boolean(param, descr):
        if type(param).__name__ != "bool":
            raise ValueError(
                descr + " {} not supported, should be bool type".format(param)
            )

    @staticmethod
    def check_open_unit_interval(param, descr):
        if type(param).__name__ not in ["float"] or param <= 0 or param >= 1:
            raise ValueError(
                descr + " should be a numeric number between 0 and 1 exclusively"
            )

    @staticmethod
    def check_valid_value(param, descr, valid_values):
        if param not in valid_values:
            raise ValueError(
                descr
                + " {} is not supported, it should be in {}".format(param, valid_values)
            )

    @staticmethod
    def check_defined_type(param, descr, types):
        if type(param).__name__ not in types:
            raise ValueError(
                descr + " {} not supported, should be one of {}".format(param, types)
            )

    @staticmethod
    def check_and_change_lower(param, valid_list, descr=""):
        if type(param).__name__ != "str":
            raise ValueError(
                descr
                + " {} not supported, should be one of {}".format(param, valid_list)
            )

        lower_param = param.lower()
        if lower_param in valid_list:
            return lower_param
        else:
            raise ValueError(
                descr
                + " {} not supported, should be one of {}".format(param, valid_list)
            )

    @staticmethod
    def _greater_equal_than(value, limit):
        return value >= limit - settings.FLOAT_ZERO

    @staticmethod
    def _less_equal_than(value, limit):
        return value <= limit + settings.FLOAT_ZERO

    @staticmethod
    def _range(value, ranges):
        in_range = False
        for left_limit, right_limit in ranges:
            if (
                    left_limit - settings.FLOAT_ZERO
                    <= value
                    <= right_limit + settings.FLOAT_ZERO
            ):
                in_range = True
                break

        return in_range

    @staticmethod
    def _in(value, right_value_list):
        return value in right_value_list

    @staticmethod
    def _not_in(value, wrong_value_list):
        return value not in wrong_value_list

    def _warn_deprecated_param(self, param_name, descr):
        if self._deprecated_params_set.get(param_name):
            logging.warning(
                f"{descr} {param_name} is deprecated and ignored in this version."
            )

    def _warn_to_deprecate_param(self, param_name, descr, new_param):
        if self._deprecated_params_set.get(param_name):
            logging.warning(
                f"{descr} {param_name} will be deprecated in future release; "
                f"please use {new_param} instead."
            )
            return True
        return False
这里运用到json 为什么要用json?
  • 方便序列化json.dumps()将对象转换为JSON字符串,可以轻松地保存或传输对象数据。
  • 方便调试和展示:在 __str__ 方法中返回 JSON 字符串,可以方便地查看对象的内容。尤其是在调试过程中,直接打印出对象的 JSON 格式可以帮助开发人员快速查看对象的状态及其内部数据结构
  • 与大模型交互:当涉及到与大语言模型(如 GPT)或其他机器学习模型的交互时,JSON 格式的数据通常是标准的输入和输出格式。将对象转为 JSON 字符串,能够更容易地将数据传递给模型进行处理或分析,模型通常会接受 JSON 格式的数据进行训练或推理。
如何去运用?
  • 数据交换:可以将这个 JSON 字符串用作 API 请求或响应的数据格式。例如,你可以将这个对象作为 HTTP 请求的 body 发送,或者从网络中获取 JSON 格式的数据,然后解析回对象。
  • 持久化存储:如果你需要将对象数据持久化到数据库或文件系统,JSON 是一个很好的存储格式。例如,将对象数据存储在文件中或数据库表的 JSON 类型字段中,便于未来读取和操作。
  • 配置和参数更新update 方法中接收到的 conf 参数实际上是一个字典对象,它通过递归的方式更新对象的属性。如果你将对象序列化为 JSON 格式后,可以将 JSON 作为配置文件传递给应用,应用根据该配置动态调整其行为。这使得系统更加灵活和可配置。
  • 验证与校验validate 方法利用存储在 JSON 文件中的规则对对象进行参数验证,这是一种常见的方式来确保输入的数据符合特定的格式或限制。可以通过动态加载配置文件来验证对象的数据,保证数据的合法性。

怎么和大模型交互 json是以什么规范 是否让机器更好的理解?

如果自己开发了一个代理组件后如何去融入呢?

index.tsx

路径web/src/pages/flow/form/baidu-fanyi-form/index.tsx

BaiduFanyiForm 是一个用于配置百度翻译(Baidu Fanyi)接口参数的表单组件。该组件基于 Ant Design (antd) 的 Form 组件,允许用户输入翻译所需的 AppID、密钥、翻译类型、领域、源语言和目标语言

// useTranslate('flow'):自定义 Hook,提供国际化翻译能力
import { useTranslate } from '@/hooks/common-hooks';
// Form, Input, Select:Ant Design 表单组件
import { Form, Input, Select } from 'antd';
// useMemo:优化计算、提高性能,避免不必要的重新计算
import { useMemo } from 'react';
// 从 constant 中引入翻译领域和语言选项
import {
  BaiduFanyiDomainOptions,
  BaiduFanyiSourceLangOptions,
} from '../../constant';
// 表单组件的类型定义,包含 onValuesChange, form, node 等参数
import { IOperatorForm } from '../../interface';
// 动态输入组件,可能用于变量替换
import DynamicInputVariable from '../components/dynamic-input-variable';

/*
生成 trans_type(翻译类型)的 Select 选项:
translate:普通翻译
fieldtranslate:专业领域翻译
选项的 label 通过 t() 进行国际化翻译。
*/
const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => {
  const { t } = useTranslate('flow');
  const options = useMemo(() => {
    return ['translate', 'fieldtranslate'].map((x) => ({
      value: x,
      label: t(`baiduSecretKeyOptions.${x}`),
    }));
  }, [t]);
// 生成 领域翻译 选项(如医学、法律等)
  const baiduFanyiOptions = useMemo(() => {
    return BaiduFanyiDomainOptions.map((x) => ({
      value: x,
      label: t(`baiduDomainOptions.${x}`),
    }));
  }, [t]);
// 源语言(source_lang)和 目标语言(target_lang)
  const baiduFanyiSourceLangOptions = useMemo(() => {
    return BaiduFanyiSourceLangOptions.map((x) => ({
      value: x,
      label: t(`baiduSourceLangOptions.${x}`),
    }));
  }, [t]);

  return (
    <Form
      name="basic" //表单名称
      autoComplete="off" //关闭自动填充
      form={form} //绑定 Ant Design 表单实例,允许动态设置表单值
      onValuesChange={onValuesChange} //表单值变更时触发外部回调
      layout={'vertical'}
    >
          <!-- 允许用户输入 AppID 和 密钥,用于身份验证 -->
      <DynamicInputVariable node={node}></DynamicInputVariable>
      <Form.Item label={t('appid')} name={'appid'}>
        <Input></Input>
      </Form.Item>
      <Form.Item label={t('secretKey')} name={'secret_key'}>
        <Input></Input>
      </Form.Item>
          <!-- 翻译类型 -->
      <Form.Item label={t('transType')} name={'trans_type'}>
        <Select options={options}></Select>
      </Form.Item>
          <!-- 领域翻译 仅在 fieldtranslate 选中时显示 -->
  <!-- dependencies={['model_type']}:监听 model_type 字段的变化 -->
      <Form.Item noStyle dependencies={['model_type']}>
        {({ getFieldValue }) =>
          getFieldValue('trans_type') === 'fieldtranslate' && (
            <Form.Item label={t('domain')} name={'domain'}>
              <Select options={baiduFanyiOptions}></Select>
            </Form.Item>
          )
        }
      </Form.Item>
      <Form.Item label={t('sourceLang')} name={'source_lang'}>
        <Select options={baiduFanyiSourceLangOptions}></Select>
      </Form.Item>
      <Form.Item label={t('targetLang')} name={'target_lang'}>
        <Select options={baiduFanyiSourceLangOptions}></Select>
      </Form.Item>
    </Form>
  );
};
export default BaiduFanyiForm;

BaiduFanyiForm 组件是百度翻译 API 参数的前端配置表单,支持动态交互(如 trans_type 选择 fieldtranslate 时动态显示 domain 选项)。该组件用于某个流程(flow)系统,允许用户输入翻译 API 相关信息,并提供 国际化支持useTranslate()



_ _ init _ _.py

首先在 agent/component/__init__.py 在组件同级目录下有一个__init__.py
它的主要作用是**导入并管理各种组件,这些组件用于数据处理、信息检索、翻译、SQL执行、财经数据获取等**不同功能。其中的重要代码:

# import importlib:用于动态导入模块(在 component_class 方法中使用)
# 这些组件分布在 agent.component 模块下,提供不同的功能

'''
该函数用于动态加载组件,通过 importlib.import_module("agent.component") 导入 agent.component 模块,然后使用 getattr(m, class_name) 获取类,从而返回对应的组件类。
'''
def component_class(class_name):
    m = importlib.import_module("agent.component")
    c = getattr(m, class_name)
    return c

'''
__all__ 列表:指定模块对外暴露的所有组件
使其成为 from module import * 时可以被访问的对象。
'''
__all__ = [
    "GitHub",
    "GitHubParam",
    "BaiduFanyi",
    "BaiduFanyiParam",
]

web/src/pages/flow/flow-drawer/index.tsx

web/src/pages/flow/flow-drawer/index.tsx里有这样一行代码
import BaiduFanyiForm from '../form/baidu-fanyi-form';
该文件 index.tsx 主要用于**表单抽屉组件 (FormDrawer)**,用于在 UI 中展示不同操作节点的表单,并支持编辑、调试等功能。

代码功能概览

🔹 动态渲染不同的表单组件BaiduFanyiForm 等)
🔹 支持不同类型的操作节点(Operator)(如 BaiduFanyi, Google, Generate 等)
🔹 提供表单输入交互,并支持单步调试
🔹 使用 Drawer 组件作为侧边栏弹出窗口

★ 组件导入

import { Drawer, Flex, Form, Input } from 'antd';
import { get, isPlainObject, lowerFirst } from 'lodash';
import { Play } from 'lucide-react';
import { CloseOutlined } from '@ant-design/icons';
...

📌 作用:引入 Ant Design 组件(DrawerFormInput)、lodash 工具库以及 Play(调试按钮)、CloseOutlined(关闭按钮)。

★ 引入表单组件

# 当然不能忘了我们的百度翻译
import BaiduFanyiForm from '../form/baidu-fanyi-form';
import GoogleForm from '../form/google-form';
import GenerateForm from '../form/generate-form';
import RetrievalForm from '../form/retrieval-form';
...

📌 作用:引入 BaiduFanyiForm 组件,该组件用于百度翻译的表单交互。

✅ 这些表单组件用于不同的 Operator 操作,例如:

  • BaiduFanyiForm百度翻译表单
  • GoogleFormGoogle 搜索表单
  • GenerateForm文本生成表单
  • RetrievalForm信息检索表单

★ FormMap(操作类型与表6单组件的映射)

const FormMap = {
  [Operator.BaiduFanyi]: BaiduFanyiForm,
  [Operator.Google]: GoogleForm,
  [Operator.Generate]: GenerateForm,
  [Operator.Retrieval]: RetrievalForm,
};

📌 作用
动态匹配 Operator(操作类型)与 对应表单组件
OperatorBaiduFanyi 时,渲染 BaiduFanyiForm
可以扩展不同的表单类型

☆ FormDrawer组件

const FormDrawer = ({ visible, hideModal, node, singleDebugDrawerVisible, hideSingleDebugDrawer, showSingleDebugDrawer }: IModalProps<any> & IProps) => {

📌 作用FormDrawer 是一个 表单抽屉组件,用于展示不同类型的表单。

☆ 动态渲染表单

const OperatorForm = FormMap[operatorName] ?? EmptyContent;

📌 作用: ✅ 根据 Operator(操作类型)选择对应的表单组件
✅ 如果 Operator 不在 FormMap 中,则显示 EmptyContent(空组件)

☆ 处理表单数据

useEffect(() => {
  if (visible) {
    form.resetFields();
    form.setFieldsValue(node?.data?.form);
  }
}, [visible, form, node?.data?.form]);

📌 作用: ✅ 当抽屉可见时,重置表单并填充数据
如果 OperatorCategorize,则特殊处理分类数据

☆ 侧边栏 (Drawer)

<Drawer
  title={
    <Flex>
      <OperatorIcon name={operatorName} />
      <Input value={name} onChange={handleNameChange} />
      <Play onClick={showSingleDebugDrawer} />
      <CloseOutlined onClick={hideModal} />
    </Flex>
  }
  open={visible}
>
  <OperatorForm onValuesChange={handleValuesChange} form={form} node={node} />
</Drawer>

📌 作用: ✅ 标题区域包含操作图标、输入框、调试按钮、关闭按钮
使用 OperatorForm 组件渲染表单
点击 Play 按钮打开调试模式


★ ★ 总结一下BaiduFanyiForm相关逻辑 ★ ★
index.tsx 里,BaiduFanyiForm 被注册到 FormMap,然后通过 FormDrawer 组件

🚀 如何渲染 BaiduFanyiForm

  1. Operator.BaiduFanyi 触发 BaiduFanyiForm
  2. FormDrawer 组件动态匹配 FormMap[Operator.BaiduFanyi]
  3. BaiduFanyiForm 渲染在 Drawer

📌 结论: ✅ BaiduFanyiForm 只是众多表单组件之一,专门用于百度翻译的功能
index.tsx 主要是表单的动态加载器,统一管理所有 Operator 类型
FormDrawer 负责 UI 渲染、数据填充、调试交互等


agent/component/baidufanyi.py 【后端代码】
agent/component/_ _ init _ _.py 【初始化】
③ web/src/pages/flow/flow-drawer/index.tsx【表单抽屉组件 UI展示】
④ web/src/pages/flow/form/baidu-fanyi-form/index.tsx 【提供用户界面让用户能配置并提交参数】
⑤ web/src/pages/flow/constant.tsx 【定义一个应用程序中使用的各种图标、常量、枚举、接口、状态和函数】
⑥ web/src/locales/zh.ts 【把所有要显示的搞到zh.ts中(前端组件描述)】
⑦ web/src/pages/agent/constant.tsx【引入组件所需的各种图标、以及操作项的样式、初始值、语言等】
⑧ web/src/pages/agent/form-sheet/use-form-config-map.tsx 【表单配置映射、配置不同操作对应表单组件】
⑨ web/src/pages/agent/form/baidu-fanyi-form/index.tsx 【构建配置表单 】
⑩ web/src/pages/agent/hooks.tsx 【管理图形界面流程图 各种钩子 → 建数据处理流程】


阅读全文

RAGFlow

2025/3/5

什么是RAGFlow

RAGFlow 是一种基于生成模型的技术,常用于智能聊天系统、推荐系统和自然语言处理任务。如果你是从零基础开始学习 RAGFlow(可能指的是 Retrieval-Augmented Generation Flow 或与其相关的模型/框架),以下是一些推荐的学习路径:

1. 理解基础概念

在学习 RAGFlow 之前,你需要了解一些相关的基本概念:

  • 自然语言处理(NLP):学习 NLP 的基本概念,比如分词、词向量(Word Embedding)、序列建模等。
  • 机器学习基础:了解机器学习的基本原理,包括监督学习、无监督学习、深度学习等。
  • 深度学习框架:熟悉一些常用的深度学习框架,如 TensorFlow 和 PyTorch,它们会在训练和实现 RAGFlow 模型时用到。

ragflow/README_zh.md at main · infiniflow/ragflow

先装个win版的Docker Desktop
打开 Anaconda Prompt 先放着不管

克隆 RAGFlow 的仓库 $ git clone https://github.com/infiniflow/ragflow.git

——————————— ★ ★ ———————————

Anaconda Prompt的命令窗口里
输入 cd E:\Python\ragflow\docker
接着 E:
然后就开始 装载 查看状态

关闭 docker-compose down
装载 docker compose up -d
查看状态 docker logs -f ragflow-server
——————————— ★ ★ ———————————

在你的 docker-compose.yml 配置中,Redis 的连接问题可能是因为 RAGFlow 配置文件中的 Redis 部分使用了密码,并且 Redis 运行在 localhost 上。如果 Redis 没有设置密码且端口号是 localhost,你可以尝试以下步骤进行调整:

  1. 修改 RAGFlow 的配置:在 ragflow 服务的配置中,确保 Redis 的连接信息正确。需要确保 Redis 连接时不使用密码并且连接到 localhost

  2. 更新 service_conf.yaml 配置:根据日志,RAGFlow 连接 Redis 时使用了密码。你需要检查 service_conf.yaml 中的 Redis 配置信息,将其修改为不使用密码,并指定 localhost

    示例修改:

      host: localhost
      port: 6379
      db: 1
      password: ""
    
  3. 检查 docker-compose.yml 配置:确保 ragflow 服务的 Redis 部分没有设置密码,且连接端口是 localhost。根据你的配置,默认情况下应该是没有密码的。

  4. 重新启动服务:在修改配置后,可以重新启动 Docker 服务以使修改生效。

    执行命令:

    docker-compose down
    docker-compose up -d
    

这样,你应该能够解决 Redis 连接的相关问题。如果还有其他问题,请继续提供日志或详细信息。

如果改了配置还是不行 ↓

检查与解决步骤

1. 确保 service_conf.yaml 已正确修改

你可以进入 RAGFlow 容器,查看 RAGFlow 读取的配置是否正确:

docker exec -it ragflow-server cat /ragflow/conf/service_conf.yaml

检查 redis 配置项是否是:

  redis:
  host: ragflow-redis  
  port: 6379
  db: 1
  password: ""  

如果不是,说明 docker-compose 可能仍然使用了旧的配置,你需要手动更新。

1. 确保 service_conf.yaml 绑定到了容器

检查 docker-compose.yml 是否正确挂载 service_conf.yaml
docker-compose.yml 里应该有:

yaml复制编辑volumes:
  - ./service_conf.yaml:/ragflow/conf/service_conf.yaml

如果没有这行,说明 service_conf.yaml 没有正确挂载,导致 RAGFlow 仍然使用容器内部的默认配置。

如果你修改了 docker-compose.yml,请执行:

docker-compose down
docker-compose up -d

然后 重新检查 容器内的 service_conf.yaml 是否正确:

docker exec -it ragflow-server cat /ragflow/conf/service_conf.yaml

你需要在请求中提供正确的凭证。根据之前你提供的账号密码 elastic:infini_rag_flow,你可以尝试以下命令:

docker exec -it 3a50f571ce26 curl -u elastic:infini_rag_flow http://localhost:9200

关于RAGFlow的顶级逆天Bug解决办法>

这个不是BUG,是使用问题,如果你是如下和我一样的报错,可以参考我的解决步骤
是使用问题,我这边完美解决了这个问题,解决步骤如下:
问题: docker logs -f 917c48df8473(infiniflow/ragflow:v0.16.0的容器日志)
peewee.OperationalError: (1045, “Access denied for user ‘root‘@’172.24.0.6’ (using password: YES)”)

问题原因,这个镜像的mysql密码一致是默认密码,只要配置没改全就会报错

解决办法:
  1. 停止并移除现有容器
    cd ./docker
    docker compose -f docker-compose.yml down
  2. 删除或备份数据卷
    备份命令:
    cp -r /home/data/docker/volumes/docker_esdata01 /backup/docker_esdata01_backup
    cp -r /home/data/docker/volumes/docker_minio_data /backup/docker_minio_data_backup
    cp -r /home/data/docker/volumes/docker_mysql_data /backup/docker_mysql_data_backup
    cp -r /home/data/docker/volumes/docker_redis_data /backup/docker_redis_data_backup
    删除命令:
    docker volume rm docker_esdata01
    docker volume rm docker_minio_data
    docker volume rm docker_mysql_data
    docker volume rm docker_redis_data
  3. 删除未使用的网络配置
    docker network prune
  4. 修改docker/.env 、 docker/service_conf.yaml.template和conf/service_conf.yaml 这三个文件
    中所有的密码,设置成自己的,务必这三处都保持一致(重要)
  5. 重新启动服务
    cd ./docker
    [root@localhost docker]# docker compose -f docker-compose.yml up -d
    WARN[0000] The “HF_ENDPOINT” variable is not set. Defaulting to a blank string.
    WARN[0000] The “MACOS” variable is not set. Defaulting to a blank string.
    [+] Running 10/10
    ✔ Network docker_ragflow Created 0.0s
    ✔ Volume “docker_esdata01” Created 0.0s
    ✔ Volume “docker_mysql_data” Created 0.0s
    ✔ Volume “docker_minio_data” Created 0.0s
    ✔ Volume “docker_redis_data” Created 0.0s
    ✔ Container ragflow-minio Started 0.4s
    ✔ Container ragflow-es-01 Started 0.4s
    ✔ Container ragflow-redis Started 0.4s
    ✔ Container ragflow-mysql Healthy 10.9s
    ✔ Container ragflow-server Started 11.1s

6.查看日志看看,没有如下报错了:

2025-02-26 10:51:53,554 INFO 20 TaskExecutor: RAGFlow version: v0.16.0 full
2025-02-26 10:51:53,554 INFO 20 Use Elasticsearch http://es01:9200 as the doc engine.
2025-02-26 10:51:53,563 INFO 20 GET http://es01:9200/ [status:200 duration:0.007s]
2025-02-26 10:51:53,566 INFO 20 HEAD http://es01:9200/ [status:200 duration:0.003s]
2025-02-26 10:51:53,566 INFO 20 Elasticsearch http://es01:9200 is healthy.
2025-02-26 10:51:53,572 WARNING 20 Load term.freq FAIL!
2025-02-26 10:51:53,577 WARNING 20 Realtime synonym is disabled, since no redis connection.
2025-02-26 10:51:53,583 WARNING 20 Load term.freq FAIL!
2025-02-26 10:51:53,588 WARNING 20 Realtime synonym is disabled, since no redis connection.
2025-02-26 10:51:53,588 INFO 20 MAX_CONTENT_LENGTH: 134217728
2025-02-26 10:51:53,588 INFO 20 SERVER_QUEUE_MAX_LEN: 1024
2025-02-26 10:51:53,588 INFO 20 SERVER_QUEUE_RETENTION: 3600
2025-02-26 10:51:53,588 INFO 20 MAX_FILE_COUNT_PER_USER: 0
2025-02-26 10:51:53,591 WARNING 20 RedisDB.queue_info rag_flow_svr_queue got exception: no such key
2025-02-26 10:51:53,592 INFO 20 task_consumer_0 reported heartbeat: {“name”: “task_consumer_0”, “now”: “2025-02-26T10:51:53.591+08:00”, “boot_at”: “2025-02-26T10:51:53.554+08:00”, “pending”: 0, “lag”: 0, “done”: 0, “failed”: 0, “current”: null}

7.注册登录注册测试:OK

👍2

shaw/dmeta-embedding-zh
Anaconda Prompt启动: ollama pull shaw/dmeta-embedding-zh

把每个段落做成向量化的编码

阅读全文

AutoGen

2025/2/27

待完成:架构 设计思想 整个代码怎么组织起来的 怎么去满足各种需求
B站牛人UP主【DataSense】
AutoGen 技术博客系列 (一):基础介绍与入门教程这篇博客提供了对 AutoGen 的基础介绍和入门教程,包括核心 - 掘金
AutoGen 智能应用开发(一)|AutoGen 基础_哔哩哔哩_bilibili

[microsoft/autogen: A programming framework for agentic AI 🤖 PyPi: autogen-agentchat Discord: https://aka.ms/autogen-discord Office Hour: https://aka.ms/autogen-officehour] (https://github.com/microsoft/autogen)

概述

AutoGenAutoGen Studio 都是由微软开发的用于创建和管理人工智能(AI)智能体的工具,但它们在功能和目标方面存在一些差异。

AutoGen 是一款基于 AI 和大数据技术的产品设计工具,旨在通过自动提取产品需求、智能优化设计方案并生成最终的产品模型或解决方案。其核心目标是将人工干预降至最低,从而提升用户体验和系统开发效率。AutoGen 是一个开源的 Python 框架,用于定义、配置和组合 AI 代理以构建多智能体应用。它提供了一种用于描述代理行为和它们之间交互的声明性语言。AutoGen 非常灵活,可用于构建各种多智能体应用,但它需要一定的编程技能才能使用。

AutoGen Studio 是一个基于 AutoGen 框架的图形用户界面(GUI)工具。它使开发人员能够更轻松地创建和管理多智能体应用,而无需编写代码。AutoGen Studio 提供了拖放式界面和各种预构建模块,可以简化多智能体应用的开发过程。但是,AutoGen Studio 的灵活性不如 AutoGen,并且它可能不适用于需要高度定制的应用。

以下表格总结了 AutoGen 和 AutoGen Studio 的一些关键区别:

特性 AutoGen AutoGen Studio
类型 框架 GUI 工具
抽象级别 更底层 更高层
灵活度 更灵活 不太灵活
易用性 更难使用 更易使用
编程要求 需要编程技能 无需编程技能
常见用例 高度定制的多智能体应用 通用多智能体应用

总而言之,AutoGen 适合需要高度定制和灵活性的多智能体应用开发人员,而 AutoGen Studio 适合需要快速构建通用多智能体应用的开发人员。

AI + 行业 = Agent

只需要把接口复制粘贴来到AutoGen Studio的技能表
**万能API**:[天聚数行TianAPI - 应用开发者API数据调用平台] (https://www.tianapi.com/)

我们要去给智能体配置上你需要的能力!

我结合智能体我可以做到每个行业 但每个行业都有自己角色的一个定义
我要给智能体定义角色


  1. 技术基础和领域:
    • “AutoGen” 可能依赖于特定的技术,如机器学习算法、自然语言处理模型等,
      主要用于自动化代码生成、文档自动提取或数据分析任务。
    • “AiAgent” 则是人工智能领域的核心概念,涵盖从需求理解到解决方案生成的
      全生命周期管理。它能够将不同领域的需求整合起来,提供综合性的解决方案。
  2. 应用场景:
    • 在“AutoGen”的应用中,可能包括自动化代码生成、文档自动化处理、数据分析
      工具的构建等。
    • 在“AiAgent”的应用中,可能涉及需求分析和理解,数据预处理与清洗,以及生
      成生成模型(如“AutoGen”)中的产品设计生成。
  3. 功能特点:
    • “AutoGen” 可能专注于自动化、快速迭代和标准化能力,适合需要大量重复劳
      动的任务。
    • “AiAgent” 通常结合人工智能的高级算法和分布式系统,能够处理复杂、多领
      域的问题,并提供灵活的解决方案。
  4. 研究方向与应用深度:
    • 在“AutoGen”的研究中,可能侧重于特定任务的具体实现和技术优化。
    • 在“AiAgent”的研究中,可能涉及跨领域的知识融合、分布式计算环境等高级技
      术,推动了AI领域的发展。

全自动化的 AI Agents

LangChain可以实现做AI Agent
XAgent || AutoGen 是更一步进化的

如果有与图片输出相关的,帮忙加一个GenImg(“图片生成提示 Prompt“)
利用Prompt设定可以分配任务到不同的Agent上

AutoGen 是一个框架,支持使用多个代理来开发 LLM 应用程序这些代理可以相互对话来解决任务。AutoGen 代理是可定制的、可对话的,并且无缝地允许人参与其中。他们可以结合 LLM、人工输入和不同工具的各种模式运行。

AutoGen是微软的开源框架

GitHub:microsoft/autogen: A programming framework for agentic AI 🤖 PyPi: autogen-agentchat Discord: https://aka.ms/autogen-discord Office Hour: https://aka.ms/autogen-officehour

文档博客:AutoGen — AutoGen

AutoGen 主要特点

AutoGen 可以轻松构建基于多代理对话的下一代 LLM 应用程序。它简化了复杂的LLM 工作流程的编排、自动化和优化。它最大限度地提高了 LLM 模型的性能并克服了它们的弱点。
它支持复杂工作流程的多种对话模式。借助可定制和可对话的代理,开发人员可以使用 AutoGen 构建各种涉及对话自主性、代理数量和代理对话拓扑的对话模式。
它提供了一系列具有不同复杂性的工作系统。 这些系统涵盖各种领域和复杂性的广泛应用。 这演示了 AutoGen 如何轻松支持不同的对话模式。
AutoGen 提供增强的 LLM 推理。 它提供 API统一和缓存等实用程序,以及错误处理、多配置推理、上下文编程等高级使用模式。

AutoGen 抽象并实现了可对话代理,旨在通过代理间对话来解决任务。
可对话:AutoGen 中的代理是可对话的,这意味着任何代理都可以从其他代理发送和接收消息以发起或继续对话
可定制:AutoGen 中的代理可以定制以集成 LLM、人、工具或它们的组合。


★ PyCharm怎么打开.ipynb后缀文件 我需要详细步骤 我pycharm里面没有这个?

如果 PyCharm 版本较老或者插件不可用,可以用外部 Jupyter Notebook 运行:

  1. 安装 Jupyter

    打开 PyCharm 自带的 Terminal(终端)输入:

    pip install jupyter
    
  2. 在 Terminal 里启动 Jupyter

    jupyter notebook
    
  3. 在浏览器中打开 .ipynb Jupyter Notebook 会在浏览器中打开,你可以在其中编辑 .ipynb 文件。

AutoGen基础环境配置

AutoGen基础环境安装_autogen安装-CSDN博客

windows开始菜单搜索栏,搜索prompt,搜索结果中可以看应用Aanconda Powershell Prompt

接下来,我们将使用这个工具创建一个特定版本的Python环境。
在打开的命令行工具中输入如下命令,然后回车。

conda create -n lcy python=3.10

-n 后面是的lcy是环境的名称,相当于一个标识,后续要用这个环境时通过这个名称进行查找
python=3.10,是指定python的版本

接下来,输入如下命令,切换到创建好的环境。

conda activate lcy

接下来我们需要安装Autogen Studio。
那怎么安装呢?超级简单,在刚刚我们准备好的python环境中执行一个命令就好。

pip install autogen studio

但如果直接这样执行的话,因为它会访问国外的网站完成下载,所以速度非常慢,慢到不能忍受。所以我们需要让它去国内的镜像下载。通过参数-i指定国内镜像地址,我们使用阿里云的镜像。

上述命令就变成如下这样了。

pip install autogenstudio -i https://mirrors.aliyun.com/pypi/simple

下载之后,使用如下命令启动autogen studio服务。

★ (lcy) C:\Users\Pluminary> autogenstudio ui --port 6001

之后直接访问AutoGen Studio [Beta] → [127.0.0.1:6001] (http://127.0.0.1:6001/)

在AutoGen Studio中,Team Builder是一个功能模块,允许用户配置和管理不同的Agents来构建多智能体系统。下面是对您提到的几种Agents的详细分析:

AssistantAgent
  • 用途:AssistantAgent 是一种用于处理和生成对话的智能体。它可以用来创建聊天机器人、虚拟助手或任何需要与用户进行交互的应用程序。
  • 功能
    • 对话生成:能够基于用户的输入生成自然语言响应。
    • 任务执行:可以执行特定的任务,如信息查询、简单计算或执行预设的命令。
    • 上下文管理:保持对话的上下文,使得对话更加连贯和自然。
Web Surfer Agent
  • 用途:Web Surfer Agent 专门用于从互联网上检索信息。它是一个网络爬虫,能够访问网页并提取有用的信息。
  • 功能
    • 网页访问:能够访问指定的网页,获取内容。
    • 信息提取:从网页中提取结构化或非结构化数据。
    • 数据整合:将提取的数据整合到对话或应用程序中。
Verification Assistant
  • 用途:Verification Assistant 用于验证信息的准确性或执行某些检查任务。
  • 功能
    • 数据验证:检查数据的准确性,例如验证用户输入的信息是否符合特定的格式或标准。
    • 逻辑验证:执行逻辑检查,比如验证某个流程是否按照预定的规则执行。
    • 合规性检查:确保操作符合特定的行业或法律标准。
UserProxyAgent
  • 用途:UserProxyAgent 代表用户执行操作,通常用于模拟用户行为或自动化用户任务。
  • 功能
    • 行为模拟:模拟用户的行为,例如在测试环境中模拟用户操作。
    • 任务自动化:自动化重复性的用户任务,提高效率。
    • 隐私保护:在需要保护用户隐私的场景中,代替用户执行操作,减少个人信息泄露的风险。

在Team Builder中,这些Agents可以被配置和组合,以构建复杂的工作流程。例如,一个工作流程可能首先使用Web Surfer Agent从网络上获取信息,然后由AssistantAgent处理这些信息并生成响应,最后由Verification Assistant验证响应的准确性。通过这种方式,AutoGen Studio 提供了一个灵活且强大的平台,用于创建高效且可扩展的多智能体系统。



ConversableAgent 是一个综合性的智能对话系统,它能够提供个性化和高效的交流体验

AssistantAgent(助手代理)

想象一下你有一个智能助手,比如Siri或Alexa,AssistantAgent就像是这个助手的大脑。它的任务是理解和处理用户的请求,然后做出相应的回应。在AutoGen系统中,AssistantAgent负责接收用户的信息,分析这些信息,并根据预设的规则或逻辑来执行任务。比如,如果你告诉助手“我明天有个会议”,AssistantAgent就会帮你设置提醒或者添加到日程中。

UserProxyAgent(用户代理代理)

想象你在玩一个角色扮演游戏,UserProxyAgent就像是你的角色,它代表你在游戏世界中行动。在AutoGen系统中,UserProxyAgent代表用户与系统或其他用户进行交互。它可以是用户的替身,执行用户的命令,或者代表用户参与某些活动。比如,如果你在一个在线论坛上,UserProxyAgent可以帮你发帖或者回复别人的评论。

GroupChatManager(群聊管理器)

想象你在参加一个多人在线会议或群聊,GroupChatManager就像是会议的主持人或者群聊的管理员。它的职责是协调群聊中的所有参与者,确保信息流畅且有序地传递。在AutoGen系统中,GroupChatManager负责管理群聊的各个方面,比如邀请成员加入、监控聊天内容、维持聊天秩序等。如果有人在群聊中发了不合适的内容,GroupChatManager可能会介入处理。


在PyCharm中新建一个基于conda的工程文件
在命令行里输入: pip install pyautogen

本地启动 DeepSeek → C:\Users\Pluminary>ollama run deepseek-r1:1.5b

配置Open WebUI

可以测试模型的问答

先安装conda
conda create -n open-webui python=3.11.0
activate open-webui

# 开源的访问大模型管理页面
pip install open-webui
配置WebUI:启动
open-webui serve
------------------
http://localhost:8080
sentence_bert_config.json: 100%|████████████████████████████████████████████████████████████| 53.0/53.0 [00:00<?, ?B/s]
tokenizer_config.json: 100%|██████████████████████████████████████████████████████████████████| 350/350 [00:00<?, ?B/s]
vocab.txt: 100%|████████████████████████████████████████████████████████████████████| 232k/232k [00:00<00:00, 1.17MB/s]
tokenizer.json: 100%|███████████████████████████████████████████████████████████████| 466k/466k [00:00<00:00, 26.0MB/s]
special_tokens_map.json: 100%|████████████████████████████████████████████████████████████████| 112/112 [00:00<?, ?B/s]
INFO:     Started server process [18264]
INFO:     Waiting for application startup.
2025-02-28 08:29:21.807 | INFO     | open_webui.utils.logger:start_logger:140 - GLOBAL_LOG_LEVEL: INFO - {}

# 等看到端口号再去访问 18264

利用Python部署本地DeepSeek并进行连通

# 用于发送 HTTP 请求。在此代码中,我们使用它与本地部署的 DeepSeek 服务进行通信
import requests
# 用于处理 JSON 数据,尤其是在发送和接收 HTTP 请求时
import json


# 定义与 DeepSeek 服务交互的类
class DeepSeekChat:
    # 这个类负责与 DeepSeek 服务进行交互 它接受两个参数:
    '''
     model:模型名称,在这个案例中是 "deepseek-r1:1.5b"。
     base_url:DeepSeek 服务的 API 基础 URL,即本地部署的 DeepSeek 服务地址。
     
我们通常使用self作为第一个参数的名称。这是为了代码的可读性和一致性。这个约定使得其他阅读你代码的Python程序员能够立即识别出self代表的是类的实例。
    '''
    def __init__(self, model, base_url):
        self.model = model
        self.base_url = base_url

    # send_message 方法:该方法接收一个消息列表 messages
    # 并构建一个 POST 请求的有效负载(payload),包括模型名称和消息内容。messages 是一个包含多条消息的列表。
    def send_message(self, messages):
        # 构建请求数据
        payload = {
            "model": self.model,
            "messages": messages
        }
        # 设置请求头,指定请求体的内容类型为 application/json
        headers = {
            "Content-Type": "application/json"
        }

        # 发送请求到本地 DeepSeek 服务
 '''
使用 requests.post() 发送 HTTP POST 请求到 DeepSeek 服务的 API 地址 self.base_url
json.dumps(payload) 将请求体的负载 payload 转换为 JSON 格式的字符串。
 '''
        response = requests.post(self.base_url, headers=headers, data=json.dumps(payload))

        if response.status_code == 200:
            return response.json()  # 返回 DeepSeek 返回的响应
        else:
            return {"error": f"Request failed with status code {response.status_code}"}


# 定义初始化消息 定义启动对话函数
'''
此函数启动并管理与用户的对话。它定义了一个初始化的 messages 列表,其中包含一个系统消息,系统消息的角色是 "system",内容是 "You are a helpful assistant.",这将告诉模型其应扮演的角色
'''
def start_conversation():
    messages = [
        {"role": "system", "content": "You are a helpful assistant."}  # 系统消息定义角色
    ]

    # 初始化 DeepSeekChat 实例
    deepseek_chat = DeepSeekChat(
        model="deepseek-r1:1.5b",  # 使用的本地模型
        base_url="http://localhost:11434/v1/chat/completions"  # 本地服务 URL
    )
# 创建一个 DeepSeekChat 类的实例 deepseek_chat,并传入本地模型和服务的 URL
    while True:
        try:
            # 获取用户输入问题
            user_input = input("You: ")

            # 如果用户输入 "exit" 退出循环
            # 进入一个 while True 循环,不断等待用户输入
            #用户的每次输入都会被捕获并存储在 user_input 变量中。

            if user_input.lower() == "exit":
                print("Exiting the conversation.")
                break

            # 添加用户消息
             essages.append({"role": "user", "content": user_input})

            # 发送用户消息并获取模型回答 将用户的输入添加到 messages 列表中,消息的角色设置为 "user"。
            response = deepseek_chat.send_message(messages)

            # 检查响应是否包含错误
            if "error" in response:
                print(response["error"])
            else:
                # 打印 DeepSeek 模型的回答
                answer = response.get("choices", [{}])[0].get("message", {}).get("content", "No response")
                print("DeepSeek: " + answer)

        except KeyboardInterrupt:
            print("\nExiting the conversation due to user interrupt.")
            break
'''
if __name__ == "__main__": 语句保证只有当脚本作为主程序执行时才会调用 start_conversation 函数。如果该脚本被作为模块导入到其他脚本中,则不会执行该函数。
'''

if __name__ == "__main__":
    start_conversation()
假设你已经理解了 DeepSeek 的基础代码,下面的例子演示了如何使用 AutoGen 集成 DeepSeek:
import requests
from autogen import Agent, GroupChat, GroupChatManager
from autogen.models.openai import OpenAIChatCompletionClient
import json

# 定义与 DeepSeek 服务交互的类
class DeepSeekChat:
    def __init__(self, model, base_url):
        self.model = model
        self.base_url = base_url

    def send_message(self, messages):
        payload = {
            "model": self.model,
            "messages": messages
        }
        headers = {"Content-Type": "application/json"}
        response = requests.post(self.base_url, headers=headers, data=json.dumps(payload))
        
        if response.status_code == 200:
            return response.json()
        else:
            return {"error": f"Request failed with status code {response.status_code}"}

# 创建自定义代理(DeepSeek 代理)
class DeepSeekAgent(Agent):
    def __init__(self, model, base_url):
        super().__init__(name="DeepSeekAgent")
        self.chat = DeepSeekChat(model, base_url)

    def on_message(self, message):
        response = self.chat.send_message([{"role": "user", "content": message}])
        return response.get("choices", [{}])[0].get("message", {}).get("content", "No response")

# 创建用户代理
class UserAgent(Agent):
    def __init__(self):
        super().__init__(name="UserAgent")

    def on_message(self, message):
        return message

# 设置 DeepSeek 代理
deepseek_agent = DeepSeekAgent(model="deepseek-r1:1.5b", base_url="http://localhost:11434/v1/chat/completions")
user_agent = UserAgent()

# 创建群聊
group_chat = GroupChat(agents=[deepseek_agent, user_agent])
group_chat_manager = GroupChatManager(group_chat)

# 启动对话
def start_conversation():
    while True:
        try:
            user_input = input("You: ")
            if user_input.lower() == "exit":
                print("Exiting the conversation.")
                break
            
            response = group_chat_manager.handle_message(user_input)
            print(f"DeepSeek: {response}")
        
        except KeyboardInterrupt:
            print("\nExiting the conversation due to user interrupt.")
            break

if __name__ == "__main__":
    start_conversation()
阅读全文

python

2025/2/26

Python 基础初级

print函数输出数组
py3.x使用输出集合
list = ["a","b","c"]
print (list)
-------------------
["a","b","c"]
带引号的是同一行显示多条语句
print('hello'); print('world')
py代码块不能使用大括号 只能用缩进
if True:
    print('true')
else:
    print('false')
多行语句可以用斜杠( \ )
多行注释可以用 单引号 也可以用双引号
# 多引号
"""
111

1
"""

# 单引号
'''
1
1
1
'''
print输出是否换行 不换行就末尾加逗号 在py3之后就要用end=” “
x = "a"
y = "b"
print(x, end=" ")
print(y)
整型 浮点 字符串
counter = 100
miles = 100.0
name = "Runoob"

print(counter)
print(miles)
print(name)
加号是字符串连接运算符 型号是重复操作
str = "Hello World!"
print(str * 2)
print(str + "TEST")

list = ['sac', 123, 2.32, 'john', 70.2]
print list[2:] # 输出从第三个开始至列表末尾的所有元素
-------------------
Hello World!Hello World!
Hello World!TEST

[2.32, 'john', 70.2]
列表是有序的对象集合,字典是无序的对象集合

两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存储
字典用’{ }’标识,字典由索引(key)和它对应的值value组成

Python中的集合(Set)、元组(Tuple)和字典(Dictionary)是三种不同的数据结构,它们各自有不同的特点和用途:

  1. 集合(Set)
    • 无序的集合数据类型,其中每个元素都是唯一的,没有重复的元素。
    • 支持集合的数学运算,如并集、交集、差集等。
    • 用大括号 {} 或者 set() 函数来创建,但空集合只能用 set() 创建,因为 {} 创建的是空字典。
    • 例子:{1, 2, 3}
  2. 元组(Tuple)
    • 有序的集合数据类型,元素用圆括号 () 括起来。
    • 元组中的元素不可修改,即一旦创建,就不能更改其内容。
    • 元组可以包含不同类型的元素。
    • 例子:(1, "hello", 3.14)
  3. 字典(Dictionary)
    • 无序的键值对集合,每个键都是唯一的。
    • 键和值之间用冒号 : 分隔,多个键值对之间用逗号 , 分隔,整个字典包括在大括号 {} 中。
    • 键必须是不可变的数据类型,如字符串、数字或元组,而值可以是任何数据类型。
    • 可以通过键来访问、修改或删除对应的值。
    • 例子:{"name": "Alice", "age": 25}

总结区别:

  • 唯一性:集合和字典中的元素(对于集合是元素,对于字典是键)都是唯一的,而元组中的元素可以重复。
  • 可变性:集合和字典是可变的,可以添加、删除元素,而元组是不可变的。
  • 有序性:元组是有序的,可以索引访问;集合和字典是无序的,不能通过索引访问。
  • 用途:集合用于处理集合操作,字典用于存储键值对,元组用于存储不可变的有序元素
tinydict = {'name': 'runoob','code':6734, 'dept':'sales'}
print(tinydict)
print(tinydict.keys())
print(tinydict.values())
------------------------------------
{'name': 'runoob', 'code': 6734, 'dept': 'sales'}
dict_keys(['name', 'code', 'dept'])
dict_values(['runoob', 6734, 'sales'])
算术运算符

‘ 两个数相乘
‘%’ 返回除法的余数 — 取模
‘**’ 幂 返回x的y次幂 a * * b为10的20次方
*=’ c * * = a 等效于 c = c * * a

比较运算符

‘<>’ 不等于 类似于 !=

运算符 描述 实例
& 按位与运算符:参与运算的两个值,如果两个相应位都为1,则该位的结果为1,否则为0 (a & b) 输出结果 12 ,二进制解释: 0000 1100
| 按位或运算符:只要对应的二个二进位有一个为1时,结果位就为1。 (a | b) 输出结果 61 ,二进制解释: 0011 1101
^ 按位异或运算符:当两对应的二进位相异时,结果为1 (a ^ b) 输出结果 49 ,二进制解释: 0011 0001
~ 按位取反运算符:将二进制表示中的每一位取反,0 变为 1,1 变为 0。**~x** 类似于 -x-1 (~a ) 输出结果 -61 ,二进制解释: 1100 0011 (以补码形式表示),在一个有符号二进制数的补码形式。
<< 左移动运算符:运算数的各二进位全部左移若干位,由 << 右边的数字指定了移动的位数,高位丢弃,低位补0。 a << 2 输出结果 240 ,二进制解释: 1111 0000
>> 右移动运算符:把”>>”左边的运算数的各二进位全部右移若干位,**>>** 右边的数字指定了移动的位数 a >> 2 输出结果 15 ,二进制解释: 0000 1111
成员运算符

除了以上的一些运算符之外,Python还支持成员运算符,测试实例中包含了一系列的成员,包括字符串,列表或元组。

运算符 描述 实例
in 如果在指定的序列中找到值返回 True,否则返回 False。 x 在 y 序列中 , 如果 x 在 y 序列中返回 True。
not in 如果在指定的序列中没有找到值返回 True,否则返回 False。 x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。
a = 10
b = 20
list = [1,2,3,4,5];
if(a in list):
    print("a 在列表内")
else:
    print("a 不在列表内")
if(b not in list):
    print("b 不在列表内")
else:
    print("b 在列表内")
--------------------------
a 不在列表内
b 不在列表内
身份运算符
算符 描述 实例
is is 是判断两个标识符是不是引用自一个对象 x is y, 类似 id(x) == id(y) , 如果引用的是同一个对象则返回 True,否则返回 False
is not is not 是判断两个标识符是不是引用自不同对象 x is not y , 类似 **id(a) != id(b)**。如果引用的不是同一个对象则返回结果 True,否则返回 False。
a1 = 20
b1 = 20
if(a1 is b1):
    print("a1 与 b1 是相同的对象")
else:
    print("a1 与 b1 不是相同的对象")

if(a1 is not b1):
    print("a1 与 b1 不是相同的对象")
else:
    print("a1 与 b1 是相同的对象")
----------------------------------
a1 与 b1 是相同的对象
a1 与 b1 是相同的对象
while循环语句 + continue + break
count = 0
while(count < 9):
    print('The count is:', count)
    count = count + 1
print("Good bye!")
--------------------------------
The count is: 0
The count is: 1
The count is: 2
The count is: 3
The count is: 4
The count is: 5
The count is: 6
The count is: 7
The count is: 8
Good bye!
count = 0
while(count < 9):
    print('The count is:', count)
    count = count + 1
    print("Good bye!")
---------------------------------
The count is: 0
Good bye!
The count is: 1
Good bye!
The count is: 2
Good bye!
The count is: 3
Good bye!
The count is: 4
Good bye!
The count is: 5
Good bye!
The count is: 6
Good bye!
The count is: 7
Good bye!
The count is: 8
Good bye!

# continue 和 break 用法
 
i = 1
while i < 10:   
    i += 1
    if i%2 > 0:     # 非双数时跳过输出
        continue
    print i         # 输出双数2、4、6、8、10
 
i = 1
while 1:            # 循环条件为1必定成立
    print i         # 输出1~10
    i += 1
    if i > 10:     # 当i大于10时跳出循环
        break
for循环语句
for letter in 'Python':
    print('当前字母 : %s' %letter)

fruits = ['banana', 'apple',  'mango']
for fruit in fruits:
    print('当前水果 :', fruit)
print('Good bye!')
--------------------------------------
当前字母 : P
当前字母 : y
当前字母 : t
当前字母 : h
当前字母 : o
当前字母 : n
当前水果 : banana
当前水果 : apple
当前水果 : mango
Good bye!
循环嵌套

Python 语言允许在一个循环体里面嵌入另一个循环。

Python for 循环嵌套语法:
for iterating_var in sequence:
   for iterating_var in sequence:
      statements(s)
   statements(s)
Python while 循环嵌套语法:
while expression:
   while expression:
      statement(s)
   statement(s)
Python break 用法
for letter in 'python':
    if letter == 'h':
        break
    print('当前字母 :', letter)
------------------------------
当前字母 : p
当前字母 : y
当前字母 : t
Python pass 用法
在 Python 中有时候会看到一个 def 函数:

def sample(n_samples):
    pass

该处的 pass 便是占据一个位置,因为如果定义一个空函数程序会报错
当你没有想好函数的内容是可以用 pass 填充,使程序可以正常运行

-----------------------------------

在 Python3.x 的时候 pass 可以写或不写。
python2.x:

def function():
    # 空函数在Python2.x版本中pass是必须的
    pass
python3.x

def function():
    # 在Python3.x的时候pass可以写或不写
    pass
Python Number数字

Python Number 数据类型用于存储数值。
数据类型是不允许改变的,这就意味着如果改变 Number 数据类型的值,将重新分配内存空间。
以下实例在变量赋值时 Number 对象将被创建:

var1 = 1
var2 = 10

您也可以使用del语句删除一些 Number 对象引用。
del语句的语法是:

del var1[,var2[,var3[....,varN]]]]

您可以通过使用del语句删除单个或多个对象,例如:

del var
del var_a, var_b

Python 支持四种不同的数值类型:

  • 整型(Int) - 通常被称为是整型或整数,是正或负整数,不带小数点。

  • 长整型(long integers) - 无限大小的整数,整数最后是一个大写或小写的L。

  • 浮点型(floating point real values) - 浮点型由整数部分与小数部分组成,浮点型也可以使用科学计数法表示(2.5e2 = 2.5 x 102 = 250)

  • 复数(complex numbers) - 复数由实数部分和虚数部分构成,可以用a + bj,或者complex(a,b)表示, 复数的实部a和虚部b都是浮点型。

Python Number 类型转换
int(x [,base ])         将x转换为一个整数  
long(x [,base ])        将x转换为一个长整数  
float(x )               将x转换到一个浮点数  
complex(real [,imag ])  创建一个复数  
str(x )                 将对象 x 转换为字符串  
repr(x )                将对象 x 转换为表达式字符串  
eval(str )              用来计算在字符串中的有效Python表达式,并返回一个对象  
tuple(s )               将序列 s 转换为一个元组  
list(s )                将序列 s 转换为一个列表  
chr(x )                 将一个整数转换为一个字符  
unichr(x )              将一个整数转换为Unicode字符  
ord(x )                 将一个字符转换为它的整数值  
hex(x )                 将一个整数转换为一个十六进制字符串  
oct(x )                 将一个整数转换为一个八进制字符串  
Python math 模块、cmath 模块

Python 中数学运算常用的函数基本都在 math 模块、cmath 模块中。
Python math 模块提供了许多对浮点数的数学运算函数。
Python cmath 模块包含了一些用于复数运算的函数。
cmath 模块的函数跟 math 模块函数基本一致,区别是 cmath 模块运算的是复数
math 模块运算的是数学运算。
要使用 math 或 cmath 函数必须先导入:

查看 math 查看包中的内容:

>>> import math
>>> dir(math)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']
>>>

下文会介绍各个函数的具体应用。

查看 cmath 查看包中的内容

>>> import cmath
>>> dir(cmath)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh', 'cos', 'cosh', 'e', 'exp', 'inf', 'infj', 'isclose', 'isfinite', 'isinf', 'isnan', 'log', 'log10', 'nan', 'nanj', 'phase', 'pi', 'polar', 'rect', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau']
>>>
Python数学函数
函数 返回值 ( 描述 )
abs(x) 返回数字的绝对值,如abs(-10) 返回 10
ceil(x) 返回数字的上入整数,如math.ceil(4.1) 返回 5
cmp(x, y) 如果 x < y 返回 -1, 如果 x == y 返回 0, 如果 x > y 返回 1
exp(x) 返回e的x次幂(ex),如math.exp(1) 返回2.718281828459045
fabs(x) 以浮点数形式返回数字的绝对值,如math.fabs(-10) 返回10.0
floor(x) 返回数字的下舍整数,如math.floor(4.9)返回 4
log(x) 如math.log(math.e)返回1.0,math.log(100,10)返回2.0
log10(x) 返回以10为基数的x的对数,如math.log10(100)返回 2.0
max(x1, x2,…) 返回给定参数的最大值,参数可以为序列。
min(x1, x2,…) 返回给定参数的最小值,参数可以为序列。
modf(x) 返回x的整数部分与小数部分,两部分的数值符号与x相同,整数部分以浮点型表示。
pow(x, y) x**y 运算后的值。
[round(x ,n]) 返回浮点数x的四舍五入值,如给出n值,则代表舍入到小数点后的位数。
sqrt(x) 返回数字x的平方根
Python随机数函数

随机数可以用于数学,游戏,安全等领域中,还经常被嵌入到算法中,用以提高算法效率,并提高程序的安全性。

Python包含以下常用随机数函数:

函数 描述
choice(seq) 从序列的元素中随机挑选一个元素,比如random.choice(range(10)),从0到9中随机挑选一个整数。
[randrange (start,] stop [,step]) 从指定范围内,按指定基数递增的集合中获取一个随机数,基数默认值为 1
random() 随机生成下一个实数,它在[0,1)范围内。
seed([x]) 改变随机数生成器的种子seed。如果你不了解其原理,你不必特别去设定seed,Python会帮你选择seed。
shuffle(lst) 将序列的所有元素随机排序
uniform(x, y) 随机生成下一个实数,它在[x,y]范围内。
Python数学常量
常量 描述
pi 数学常量 pi(圆周率,一般以π来表示)
e 数学常量 e,e即自然常数(自然常数)。
Python 字符串

字符串是 Python 中最常用的数据类型。我们可以使用引号 ( ) 来创建字符串。
创建字符串很简单,只要为变量分配一个值即可。例如:

var1 = 'Hello World!'
var2 = "Python Runoob"
Python 字符串格式化符号
符 号 描述
%c 格式化字符及其ASCII码
%s 格式化字符串
%d 格式化整数
%u 格式化无符号整型
%o 格式化无符号八进制数
%x 格式化无符号十六进制数
%X 格式化无符号十六进制数(大写)
%f 格式化浮点数字,可指定小数点后的精度
%e 用科学计数法格式化浮点数
%E 作用同%e,用科学计数法格式化浮点数
%g %f和%e的简写
%G %F 和 %E 的简写
%p 用十六进制数格式化变量的地址
格式化操作符辅助指令:
符号 功能
* 定义宽度或者小数点精度
- 用做左对齐
+ 在正数前面显示加号( + )
在正数前面显示空格
# 在八进制数前面显示零(‘0’),在十六进制前面显示’0x’或者’0X’(取决于用的是’x’还是’X’)
0 显示的数字前面填充’0’而不是默认的空格
% ‘%%’输出一个单一的’%’
(var) 映射变量(字典参数)
m.n. m 是显示的最小总宽度,n 是小数点后的位数(如果可用的话)
Python 的字符串内建函数

字符串方法是从 Python1.6 到 2.0 慢慢加进来的 —— 它们也被加到了Jython 中。

这些方法实现了 string 模块的大部分方法,如下表所示列出了目前字符串内建支持的方法,所有的方法都包含了对 Unicode 的支持,有一些甚至是专门用于 Unicode 的。

方法 描述
string.capitalize() 把字符串的第一个字符大写
string.center(width) 返回一个原字符串居中,并使用空格填充至长度 width 的新字符串
string.count(str, beg=0, end=len(string)) 返回 str 在 string 里面出现的次数,如果 beg 或者 end 指定则返回指定范围内 str 出现的次数
string.decode(encoding=’UTF-8’, errors=’strict’) 以 encoding 指定的编码格式解码 string,如果出错默认报一个 ValueError 的 异 常 , 除非 errors 指 定 的 是 ‘ignore’ 或 者’replace’
string.encode(encoding=’UTF-8’, errors=’strict’) 以 encoding 指定的编码格式编码 string,如果出错默认报一个ValueError 的异常,除非 errors 指定的是’ignore’或者’replace’
string.endswith(obj, beg=0, end=len(string)) 检查字符串是否以 obj 结束,如果beg 或者 end 指定则检查指定的范围内是否以 obj 结束,如果是,返回 True,否则返回 False.
string.expandtabs(tabsize=8) 把字符串 string 中的 tab 符号转为空格,tab 符号默认的空格数是 8。
string.find(str, beg=0, end=len(string)) 检测 str 是否包含在 string 中,如果 beg 和 end 指定范围,则检查是否包含在指定范围内,如果是返回开始的索引值,否则返回-1
string.format() 格式化字符串
string.index(str, beg=0, end=len(string)) 跟find()方法一样,只不过如果str不在 string中会报一个异常.
string.isalnum() 如果 string 至少有一个字符并且所有字符都是字母或数字则返回 True,否则返回 False
string.isalpha() 如果 string 至少有一个字符并且所有字符都是字母则返回 True,否则返回 False
string.isdecimal() 如果 string 只包含十进制数字则返回 True 否则返回 False.
string.isdigit() 如果 string 只包含数字则返回 True 否则返回 False.
string.islower() 如果 string 中包含至少一个区分大小写的字符,并且所有这些(区分大小写的)字符都是小写,则返回 True,否则返回 False
string.isnumeric() 如果 string 中只包含数字字符,则返回 True,否则返回 False
string.isspace() 如果 string 中只包含空格,则返回 True,否则返回 False.
string.istitle() 如果 string 是标题化的(见 title())则返回 True,否则返回 False
string.isupper() 如果 string 中包含至少一个区分大小写的字符,并且所有这些(区分大小写的)字符都是大写,则返回 True,否则返回 False
string.join(seq) 以 string 作为分隔符,将 seq 中所有的元素(的字符串表示)合并为一个新的字符串
string.ljust(width) 返回一个原字符串左对齐,并使用空格填充至长度 width 的新字符串
string.lower() 转换 string 中所有大写字符为小写.
string.lstrip() 截掉 string 左边的空格
string.maketrans(intab, outtab) maketrans() 方法用于创建字符映射的转换表,对于接受两个参数的最简单的调用方式,第一个参数是字符串,表示需要转换的字符,第二个参数也是字符串表示转换的目标。
max(str) 返回字符串 str 中最大的字母。
min(str) 返回字符串 str 中最小的字母。
string.partition(str) 有点像 find()和 split()的结合体,从 str 出现的第一个位置起,把 字 符 串 string 分 成 一 个 3 元 素 的 元 组 (string_pre_str,str,string_post_str),如果 string 中不包含str 则 string_pre_str == string.
string.replace(str1, str2, num=string.count(str1)) 把 string 中的 str1 替换成 str2,如果 num 指定,则替换不超过 num 次.
string.rfind(str, beg=0,end=len(string) ) 类似于 find() 函数,返回字符串最后一次出现的位置,如果没有匹配项则返回 -1。
string.rindex( str, beg=0,end=len(string)) 类似于 index(),不过是返回最后一个匹配到的子字符串的索引号。
string.rjust(width) 返回一个原字符串右对齐,并使用空格填充至长度 width 的新字符串
string.rpartition(str) 类似于 partition()函数,不过是从右边开始查找
string.rstrip() 删除 string 字符串末尾的空格.
string.split(str=””, num=string.count(str)) 以 str 为分隔符切片 string,如果 num 有指定值,则仅分隔 num+1 个子字符串
[string.splitlines(keepends]) 按照行(‘\r’, ‘\r\n’, ‘\n’)分隔,返回一个包含各行作为元素的列表,如果参数 keepends 为 False,不包含换行符,如果为 True,则保留换行符。
string.startswith(obj, beg=0,end=len(string)) 检查字符串是否是以 obj 开头,是则返回 True,否则返回 False。如果beg 和 end 指定值,则在指定范围内检查.
[string.strip(obj]) 在 string 上执行 lstrip()和 rstrip()
string.swapcase() 翻转 string 中的大小写
string.title() 返回”标题化”的 string,就是说所有单词都是以大写开始,其余字母均为小写(见 istitle())
string.translate(str, del=””) 根据 str 给出的表(包含 256 个字符)转换 string 的字符,要过滤掉的字符放到 del 参数中
string.upper() 转换 string 中的小写字母为大写
string.zfill(width) 返回长度为 width 的字符串,原字符串 string 右对齐,前面填充0
访问列表中的值 && 追加列表【append】&& 删除列表元素【del】
print("----------------")
list1 = ['physics', 'chemistry', 1997, 2000]
print(list1)
print(list1[0])
list1.append('Google')
print(list1)

del list1[4] #删除了Google
print(list1)
-------------------------------
['physics', 'chemistry', 1997, 2000]
physics
['physics', 'chemistry', 1997, 2000, 'Google']
['physics', 'chemistry', 1997, 2000]  #删除了Google
Python列表脚本操作符

列表对 + 和 * 的操作符与字符串相似。+ 号用于组合列表,* 号用于重复列表。

Python 表达式 结果 描述
len([1, 2, 3]) 3 长度
[1, 2, 3] + [4, 5, 6] [1, 2, 3, 4, 5, 6] 组合
[‘Hi!’] * 4 [‘Hi!’, ‘Hi!’, ‘Hi!’, ‘Hi!’] 重复
3 in [1, 2, 3] True 元素是否存在于列表中
for x in [1, 2, 3]: print x, 1 2 3 迭代
记得负号是倒数的噢
>>>L = ['Google', 'Runoob', 'Taobao']
>>> L[2]
'Taobao'
>>> L[-2]
'Runoob'
>>> L[1:]
['Runoob', 'Taobao']
>>>
Python 表达式 结果 描述
L[2] ‘Taobao’ 读取列表中第三个元素
L[-2] ‘Runoob’ 读取列表中倒数第二个元素
L[1:] [‘Runoob’, ‘Taobao’] 从第二个元素开始截取列表
Python列表函数&方法
序号 函数
1 cmp(list1, list2) 比较两个列表的元素
2 len(list) 列表元素个数
3 max(list) 返回列表元素最大值
4 min(list) 返回列表元素最小值
5 list(seq) 将元组转换为列表
序号 方法
1 list.append(obj) 在列表末尾添加新的对象
2 list.count(obj) 统计某个元素在列表中出现的次数
3 list.extend(seq) 在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)
4 list.index(obj) 从列表中找出某个值第一个匹配项的索引位置
5 list.insert(index, obj) 将对象插入列表
6 [list.pop(index=-1]) 移除列表中的一个元素(默认最后一个元素),并且返回该元素的值
7 list.remove(obj) 移除列表中某个值的第一个匹配项
8 list.reverse() 反向列表中元素
9 list.sort(cmp=None, key=None, reverse=False) 对原列表进行排序
Python日期和时间

Python 程序能用很多方式处理日期和时间,转换日期格式是一个常见的功能。
Python 提供了一个 time 和 calendar 模块可以用于格式化日期和时间。
时间间隔是以秒为单位的浮点小数。
每个时间戳都以自从1970年1月1日午夜(历元)经过了多长时间来表示。
Python 的 time 模块下有很多函数可以转换常见日期格式。
如函数time.time()用于获取当前时间戳, 如下实例:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
import time  # 引入time模块
 
ticks = time.time()
print "当前时间戳为:", ticks
--------------------------
当前时间戳为: 1459994552.51
获取某月日历

time.time( ) 返回当前时间的时间戳(1970纪元后经过的浮点秒数)
time.sleep(secs) 推迟调用线程的运行,secs指秒数。
time.localtime([secs]) 接收时间戳(1970纪元后经过的浮点秒数)并返回当地时间下的时间元组t(t.tm_isdst可取0或1,取决于当地当时是不是夏令时)。

import  calendar
cal = calendar.month(2025, 2)
print("以下输出是 2025 年 2 月的日历:")
print(cal)
------------------------------------
以下输出是 2025 年 2 月的日历:
   February 2025
Mo Tu We Th Fr Sa Su
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28
Python函数

你可以定义一个由自己想要功能的函数,以下是简单的规则:

  • 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号**()**。
  • 任何传入参数和自变量必须放在圆括号中间。圆括号之间可以用于定义参数。
  • 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
  • 函数内容以冒号起始,并且缩进。
  • return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。
def functionname( parameters ):
   "函数_文档字符串"
   function_suite
   return [expression]

默认情况下,参数值和参数名称是按函数声明中定义的顺序匹配起来的。

def readme(str):
    print(str)
    return
readme("Hello World!")
readme("Runoob")
----------------
Hello World!
Runoob
不定长参数

你可能需要一个函数能处理比当初声明时更多的参数。这些参数叫做不定长参数,和上述2种参数不同,声明时不会命名。基本语法如下:

def functionname([formal_args,] *var_args_tuple ):
   "函数_文档字符串"
   function_suite
   return [expression]

加了星号(*)的变量名会存放所有未命名的变量参数。不定长参数实例如下:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 可写函数说明
def printinfo( arg1, *vartuple ):
   "打印任何传入的参数"
   print "输出: "
   print arg1
   for var in vartuple:
      print var
   return
 
# 调用printinfo 函数
printinfo( 10 )
printinfo( 70, 60, 50 )
------------------------------
输出:
10
输出:
70
60
50
匿名函数

python 使用 lambda 来创建匿名函数。

  • lambda只是一个表达式,函数体比def简单很多。
  • lambda的主体是一个表达式,而不是一个代码块。仅仅能在lambda表达式中封装有限的逻辑进去。
  • lambda函数拥有自己的命名空间,且不能访问自有参数列表之外或全局命名空间里的参数。
  • 虽然lambda函数看起来只能写一行,却不等同于C或C++的内联函数,后者的目的是调用小函数时不占用栈内存从而增加运行效率。

lambda函数的语法只包含一个语句,如下:

lambda [arg1 [,arg2,.....argn]]:expressio

实例如下:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 可写函数说明
sum = lambda arg1, arg2: arg1 + arg2
 
# 调用sum函数
print "相加后的值为 : ", sum( 10, 20 )
print "相加后的值为 : ", sum( 20, 20 )
---------------------------------------
相加后的值为 :  30
相加后的值为 :  40

Python 中的 lambda 表达式是一种匿名函数,即没有名字的函数。lambda 表达式通常用于需要短小精悍函数的场景,比如排序时的键函数,或者作为高阶函数的参数。

lambda 表达式的语法如下:

lambda arguments: expression

这里的关键点如下:

  • lambda 是一个关键字,用于定义匿名函数。
  • arguments 是函数的参数,可以有多个,用逗号分隔。
  • expression 是一个表达式,其结果将作为函数的返回值。

以下是一些 lambda 表达式的使用示例:

示例 1:定义一个简单的加法函数

add = lambda x, y: x + y
print(add(5, 3))  # 输出 8

示例 2:列表排序

使用 lambda 表达式来按照列表中元素的某个属性进行排序:

# 假设有一个包含字典的列表
list_of_dicts = [{'name': 'Alice', 'age': 25}, {'name': 'Bob', 'age': 20}, {'name': 'Charlie', 'age': 30}]

# 按照年龄排序
sorted_list = sorted(list_of_dicts, key=lambda x: x['age'])
print(sorted_list)  # 输出 [{'name': 'Bob', 'age': 20}, {'name': 'Alice', 'age': 25}, {'name': 'Charlie', 'age': 30}]

示例 3:高阶函数中的使用

lambda 表达式常用于 map, filter, reduce 等高阶函数:

# 使用 map 函数将列表中的每个元素乘以2
numbers = [1, 2, 3, 4, 5]
doubled = map(lambda x: x * 2, numbers)
print(list(doubled))  # 输出 [2, 4, 6, 8, 10]

# 使用 filter 函数过滤出列表中的偶数
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers))  # 输出 [2, 4]

需要注意的是,虽然 lambda 表达式非常方便,但它们通常只用于简单的函数。如果函数逻辑较为复杂,最好还是使用常规的 def 语句来定义函数,这样可以提高代码的可读性和可维护性。

return 语句

return语句[表达式]退出函数,选择性地向调用方返回一个表达式。不带参数值的return语句返回None。之前的例子都没有示范如何返回数值,下例便告诉你怎么做:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 可写函数说明
def sum( arg1, arg2 ):
   # 返回2个参数的和."
   total = arg1 + arg2
   print "函数内 : ", total
   return total
 
# 调用sum函数
total = sum( 10, 20 )
全局变量和局部变量

定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。

局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中。如下实例:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
total = 0 # 这是一个全局变量
# 可写函数说明
def sum( arg1, arg2 ):
   #返回2个参数的和."
   total = arg1 + arg2 # total在这里是局部变量.
   print "函数内是局部变量 : ", total
   return total
 
#调用sum函数
sum( 10, 20 )
print "函数外是全局变量 : ", total
------------------------------------------
函数内是局部变量 :  30
函数外是全局变量 :  0
from…import 语句

Python 的 from 语句让你从模块中导入一个指定的部分到当前命名空间中。语法如下:

from modname import name1[, name2[, ... nameN]]

例如,要导入模块 fib 的 fibonacci 函数,使用如下语句:

from fib import fibonacci

这个声明不会把整个 fib 模块导入到当前的命名空间中,它只会将 fib 里的 fibonacci 单个引入到执行这个声明的模块的全局符号表。

from…import* 语句

把一个模块的所有内容全都导入到当前的命名空间也是可行的,只需使用如下声明:

from modname import *

这提供了一个简单的方法来导入一个模块中的所有项目。然而这种声明不该被过多地使用。

例如我们想一次性引入 math 模块中所有的东西,语句如下:

from math import *
globals() 和 locals() 函数

根据调用地方的不同,globals() 和 locals() 函数可被用来返回全局和局部命名空间里的名字。

如果在函数内部调用 locals(),返回的是所有能在该函数里访问的命名。

如果在函数内部调用 globals(),返回的是所有在该函数里能访问的全局名字。

两个函数的返回类型都是字典。所以名字们能用 keys() 函数摘取。


读取键盘输入

Python提供了两个内置函数从标准输入读入一行文本,默认的标准输入是键盘。如下:

  • raw_input
  • input

input函数

input([prompt]) 函数从标准输入读取一个行,并返回一个字符串(去掉结尾的换行符)

请输入:23
你输入的内容是:  23
打开和关闭文件

您已经可以向标准输入和输出进行读写。现在,来看看怎么读写实际的数据文件。
Python 提供了必要的函数和方法进行默认情况下的文件基本操作。你可以用 file 对象做大部分的文件操作

open 函数

你必须先用Python内置的open()函数打开一个文件,创建一个file对象,相关的方法才可以调用它进行读写。

语法:

file object = open(file_name [, access_mode][, buffering])

各个参数的细节如下:

  • file_name:file_name变量是一个包含了你要访问的文件名称的字符串值。
  • access_mode:access_mode决定了打开文件的模式:只读,写入,追加等。所有可取值见如下的完全列表。这个参数是非强制的,默认文件访问模式为只读(r)。
  • buffering:如果buffering的值被设为0,就不会有寄存。如果buffering的值取1,访问文件时会寄存行。如果将buffering的值设为大于1的整数,表明了这就是的寄存区的缓冲大小。如果取负值,寄存区的缓冲大小则为系统默认。

不同模式打开文件的完全列表:

模式 描述
t 文本模式 (默认)。
x 写模式,新建一个文件,如果该文件已存在则会报错。
b 二进制模式。
+ 打开一个文件进行更新(可读可写)。
U 通用换行模式(不推荐)。
r 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
rb 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。一般用于非文本文件如图片等。
r+ 打开一个文件用于读写。文件指针将会放在文件的开头。
rb+ 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。一般用于非文本文件如图片等。
w 打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb 以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
w+ 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb+ 以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
a 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
ab 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
a+ 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
ab+ 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写
模式 r r+ w w+ a a+
+ + + +
+ + + + +
创建 + + + +
覆盖 + +
指针在开始 + + + +
指针在结尾 + +
File对象的属性

一个文件被打开后,你有一个file对象,你可以得到有关该文件的各种信息。

以下是和file对象相关的所有属性的列表:

属性 描述
file.closed 返回true如果文件已被关闭,否则返回false。
file.mode 返回被打开文件的访问模式。
file.name 返回文件的名称。
file.softspace 如果用print输出后,必须跟一个空格符,则返回false。否则返回true。

以上实例输出结果:

文件名:  foo.txt
是否已关闭 :  False
访问模式 :  w
末尾是否强制加空格 :  0
close()方法

File 对象的 close()方法刷新缓冲区里任何还没写入的信息,并关闭该文件,这之后便不能再进行写入。

当一个文件对象的引用被重新指定给另一个文件时,Python 会关闭之前的文件。用 close()方法关闭文件是一个很好的习惯。

语法:

fileObject.close()

例子:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 打开一个文件
fo = open("foo.txt", "w")
print "文件名: ", fo.name
 
# 关闭打开的文件
fo.close()

以上实例输出结果:

文件名:  foo.txt
write()方法

write()方法可将任何字符串写入一个打开的文件。需要重点注意的是,Python字符串可以是二进制数据,而不是仅仅是文字。

write()方法不会在字符串的结尾添加换行符(‘\n’):

语法:

fileObject.write(string)

在这里,被传递的参数是要写入到已打开文件的内容。

例子:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
# 打开一个文件
fo = open("foo.txt", "w")
fo.write( "www.runoob.com!\nVery good site!\n")
 
# 关闭打开的文件
fo.close()
read()方法

read()方法从一个打开的文件中读取一个字符串。需要重点注意的是,Python字符串可以是二进制数据,而不是仅仅是文字。

语法:

fileObject.read([count])

在这里,被传递的参数是要从已打开文件中读取的字节计数。该方法从文件的开头开始读入,如果没有传入count,它会尝试尽可能多地读取更多的内容,很可能是直到文件的末尾。

例子:

这里我们用到以上创建的 foo.txt 文件。

实例:

\#!/usr/bin/python
\# -*- coding: UTF-8 -*-

\# 打开一个文件
fo = open("foo.txt", "r+")
str = fo.read(10)
**print** "读取的字符串是 : ", str
\# 关闭打开的文件
fo.close()
---------------------
读取的字符串是 :  www.runoob
重命名和删除文件

Python的os模块提供了帮你执行文件处理操作的方法,比如重命名和删除文件。
要使用这个模块,你必须先导入它,然后才可以调用相关的各种功能。

rename() 方法

rename() 方法需要两个参数,当前的文件名和新文件名。

语法:

os.rename(current_file_name, new_file_name)

例子:

下例将重命名一个已经存在的文件test1.txt。

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import os
 
# 重命名文件test1.txt到test2.txt。
os.rename( "test1.txt", "test2.txt" )
remove()方法

你可以用remove()方法删除文件,需要提供要删除的文件名作为参数。

语法:

os.remove(file_name)

例子:

下例将删除一个已经存在的文件test2.txt。

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import os
 
# 删除一个已经存在的文件test2.txt
os.remove("test2.txt")
mkdir()方法

可以使用os模块的mkdir()方法在当前目录下创建新的目录们。你需要提供一个包含了要创建的目录名称的参数。

语法:

os.mkdir("newdir")

例子:

下例将在当前目录下创建一个新目录test。

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import os
 
# 创建目录test
os.mkdir("test")
chdir()方法

可以用chdir()方法来改变当前的目录。chdir()方法需要的一个参数是你想设成当前目录的目录名称。

语法:

os.chdir("newdir")

例子:

下例将进入”/home/newdir”目录。

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import os
 
# 将当前目录改为"/home/newdir"
os.chdir("/home/newdir")
getcwd() 方法

getcwd()方法显示当前的工作目录。

语法:

os.getcwd()

例子:

下例给出当前目录:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import os
 
# 给出当前的目录
print os.getcwd()
rmdir()方法

rmdir()方法删除目录,目录名称以参数传递。

在删除这个目录之前,它的所有内容应该先被清除。

语法:

os.rmdir('dirname')

例子:

以下是删除” /tmp/test”目录的例子。目录的完全合规的名称必须被给出,否则会在当前目录下搜索该目录。

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import os
 
# 删除”/tmp/test”目录
os.rmdir( "/tmp/test"  )
Python File(文件) 方法
open() 方法

Python open() 方法用于打开一个文件,并返回文件对象,在对文件进行处理过程都需要使用到这个函数,如果该文件无法被打开,会抛出 OSError。

注意:使用 open() 方法一定要保证关闭文件对象,即调用 close() 方法。

open() 函数常用形式是接收两个参数:文件名(file)和模式(mode)。

open(file, mode='r')

完整的语法格式为:

open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

参数说明:

  • file: 必需,文件路径(相对或者绝对路径)。
  • mode: 可选,文件打开模式
  • buffering: 设置缓冲
  • encoding: 一般使用utf8
  • errors: 报错级别
  • newline: 区分换行符
  • closefd: 传入的file参数类型
  • opener: 设置自定义开启器,开启器的返回值必须是一个打开的文件描述符。
file 对象

file 对象使用 open 函数来创建,下表列出了 file 对象常用的函数:

序号 方法及描述
1 file.close()关闭文件。关闭后文件不能再进行读写操作。
2 file.flush()刷新文件内部缓冲,直接把内部缓冲区的数据立刻写入文件, 而不是被动的等待输出缓冲区写入。
3 file.fileno()返回一个整型的文件描述符(file descriptor FD 整型), 可以用在如os模块的read方法等一些底层操作上。
4 file.isatty()如果文件连接到一个终端设备返回 True,否则返回 False。
5 file.next()返回文件下一行。
6 [file.read(size])从文件读取指定的字节数,如果未给定或为负则读取所有。
7 [file.readline(size])读取整行,包括 “\n” 字符。
8 [file.readlines(sizeint])读取所有行并返回列表,若给定sizeint>0,则是设置一次读多少字节,这是为了减轻读取压力。
9 [file.seek(offset, whence])设置文件当前位置
10 file.tell()返回文件当前位置。
11 [file.truncate(size])截取文件,截取的字节通过size指定,默认为当前文件位置。
12 file.write(str)将字符串写入文件,返回的是写入的字符长度。
13 file.writelines(sequence)向文件写入一个序列字符串列表,如果需要换行则要自己加入每行的换行符。
异常处理

捕捉异常可以使用try/except语句。

try/except语句用来检测try语句块中的错误,从而让except语句捕获异常信息并处理。

如果你不想在异常发生时结束你的程序,只需在try里捕获它。

语法:

以下为简单的try….except…else的语法:

try:
<语句>        #运行别的代码
except <名字>:
<语句>        #如果在try部份引发了'name'异常
except <名字>,<数据>:
<语句>        #如果引发了'name'异常,获得附加的数据
else:
<语句>        #如果没有异常发生

try的工作原理是,当开始一个try语句后,python就在当前程序的上下文中作标记,这样当异常出现时就可以回到这里,try子句先执行,接下来会发生什么依赖于执行时是否出现异常。

  • 如果当try后的语句执行时发生异常,python就跳回到try并执行第一个匹配该异常的except子句,异常处理完毕,控制流就通过整个try语句(除非在处理异常时又引发新的异常)。
  • 如果在try后的语句里发生了异常,却没有匹配的except子句,异常将被递交到上层的try,或者到程序的最上层(这样将结束程序,并打印默认的出错信息)。
  • 如果在try子句执行时没有发生异常,python将执行else语句后的语句(如果有else的话),然后控制流通过整个try语句。
#!/usr/bin/python
# -*- coding: UTF-8 -*-

try:
    fh = open("testfile", "w")
    fh.write("这是一个测试文件,用于测试异常!!")
except IOError:
    print "Error: 没有找到文件或读取文件失败"
else:
    print "内容写入文件成功"
    fh.close()
使用except而不带任何异常类型
try:
   # 正常的操作
   ......................
except:
   # 发生异常,执行这块代码
   ......................
else:
   # 如果没有异常执行这块代码

以上方式try-except语句捕获所有发生的异常。但这不是一个很好的方式,我们不能通过该程序识别出具体的异常信息。因为它捕获所有的异常。

使用except而带多种异常类型

你也可以使用相同的except语句来处理多个异常信息,如下所示:

try:
   # 正常的操作
   ......................
except(Exception1[, Exception2[,...ExceptionN]]):
   # 发生以上多个异常中的一个,执行这块代码
   ......................
else:
   # 如果没有异常执行这块代码
try-finally 语句

try-finally 语句无论是否发生异常都将执行最后的代码。

try:
<语句>
finally:
<语句>    #退出try时总会执行
raise

---------------------------
try:
    fh = open("testfile", "w")
    fh.write("这是一个测试文件,用于测试异常!!")
finally:
    print "Error: 没有找到文件或读取文件失败"

同样的例子也可以写成如下方式

#!/usr/bin/python
# -*- coding: UTF-8 -*-

try:
    fh = open("testfile", "w")
    try:
        fh.write("这是一个测试文件,用于测试异常!!")
    finally:
        print "关闭文件"
        fh.close()
except IOError:
    print "Error: 没有找到文件或读取文件失败"

当在try块中抛出一个异常,立即执行finally块代码。finally块中的所有语句执行后,异常被再次触发,并执行except块代码。参数的内容不同于异常。

触发异常

实例

一个异常可以是一个字符串,类或对象。 Python的内核提供的异常,大多数都是实例化的类,这是一个类的实例的参数。

定义一个异常非常简单,如下所示:

实例

def functionName( level ):
  if level < 1:
    raise Exception("Invalid level!", level)
    \# 触发异常后,后面的代码就不会再执行

为了能够捕获异常,”except”语句必须有用相同的异常来抛出类对象或者字符串。例如我们捕获以上异常,”except”语句如下所示:

# 触发异常后,后面的代码就不会再执行  触发异常后,后面的代码就不会再执行
try:
    正常逻辑
except Exception,err:
    触发自定义异常    
else:
    其余代码

----------------------
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# 定义函数
def mye( level ):
    if level < 1:
        raise Exception,"Invalid level!"
        # 触发异常后,后面的代码就不会再执行
try:
    mye(0)            # 触发异常
except Exception,err:
    print 1,err
else:
    print 2
----------------------
$ python test.py 
1 Invalid level!
用户自定义异常

通过创建一个新的异常类,程序可以命名它们自己的异常。异常应该是典型的继承自Exception类,通过直接或间接的方式。以下为与RuntimeError相关的实例,实例中创建了一个类,基类为RuntimeError,用于在异常触发时输出更多的信息。在try语句块中,用户自定义的异常后执行except块语句,变量 e 是用于创建Networkerror类的实例。

class Networkerror(RuntimeError):
    def __init__(self, arg):
        self.args = arg

在你定义以上类后,你可以触发该异常,如下所示:

try:
    raise Networkerror("Bad hostname")
except Networkerror,e:
    print e.args
Python OS 文件/目录方法
序号 方法及描述
1 os.access(path, mode) 检验权限模式
2 os.chdir(path) 改变当前工作目录
3 os.chflags(path, flags) 设置路径的标记为数字标记。
4 os.chmod(path, mode) 更改权限
5 os.chown(path, uid, gid) 更改文件所有者
6 os.chroot(path) 改变当前进程的根目录
7 os.close(fd) 关闭文件描述符 fd
8 os.closerange(fd_low, fd_high) 关闭所有文件描述符,从 fd_low (包含) 到 fd_high (不包含), 错误会忽略
9 os.dup(fd) 复制文件描述符 fd
10 os.dup2(fd, fd2) 将一个文件描述符 fd 复制到另一个 fd2
11 os.fchdir(fd) 通过文件描述符改变当前工作目录
12 os.fchmod(fd, mode) 改变一个文件的访问权限,该文件由参数fd指定,参数mode是Unix下的文件访问权限。
13 os.fchown(fd, uid, gid) 修改一个文件的所有权,这个函数修改一个文件的用户ID和用户组ID,该文件由文件描述符fd指定。
14 os.fdatasync(fd) 强制将文件写入磁盘,该文件由文件描述符fd指定,但是不强制更新文件的状态信息。
15 [os.fdopen(fd, mode[, bufsize]]) 通过文件描述符 fd 创建一个文件对象,并返回这个文件对象
16 os.fpathconf(fd, name) 返回一个打开的文件的系统配置信息。name为检索的系统配置的值,它也许是一个定义系统值的字符串,这些名字在很多标准中指定(POSIX.1, Unix 95, Unix 98, 和其它)。
17 os.fstat(fd) 返回文件描述符fd的状态,像stat()。
18 os.fstatvfs(fd) 返回包含文件描述符fd的文件的文件系统的信息,像 statvfs()
19 os.fsync(fd) 强制将文件描述符为fd的文件写入硬盘。
20 os.ftruncate(fd, length) 裁剪文件描述符fd对应的文件, 所以它最大不能超过文件大小。
21 os.getcwd() 返回当前工作目录
22 os.getcwdu() 返回一个当前工作目录的Unicode对象
23 os.isatty(fd) 如果文件描述符fd是打开的,同时与tty(-like)设备相连,则返回true, 否则False。
24 os.lchflags(path, flags) 设置路径的标记为数字标记,类似 chflags(),但是没有软链接
25 os.lchmod(path, mode) 修改连接文件权限
26 os.lchown(path, uid, gid) 更改文件所有者,类似 chown,但是不追踪链接。
27 os.link(src, dst) 创建硬链接,名为参数 dst,指向参数 src
28 os.listdir(path) 返回path指定的文件夹包含的文件或文件夹的名字的列表。
29 os.lseek(fd, pos, how) 设置文件描述符 fd当前位置为pos, how方式修改: SEEK_SET 或者 0 设置从文件开始的计算的pos; SEEK_CUR或者 1 则从当前位置计算; os.SEEK_END或者2则从文件尾部开始. 在unix,Windows中有效
30 os.lstat(path) 像stat(),但是没有软链接
31 os.major(device) 从原始的设备号中提取设备major号码 (使用stat中的st_dev或者st_rdev field)。
32 os.makedev(major, minor) 以major和minor设备号组成一个原始设备号
33 [os.makedirs(path, mode]) 递归文件夹创建函数。像mkdir(), 但创建的所有intermediate-level文件夹需要包含子文件夹。
34 os.minor(device) 从原始的设备号中提取设备minor号码 (使用stat中的st_dev或者st_rdev field )。
35 [os.mkdir(path, mode]) 以数字mode的mode创建一个名为path的文件夹.默认的 mode 是 0777 (八进制)。
36 [os.mkfifo(path, mode]) 创建命名管道,mode 为数字,默认为 0666 (八进制)
37 [os.mknod(filename, mode=0600, device]) 创建一个名为filename文件系统节点(文件,设备特别文件或者命名pipe)。
38 [os.open(file, flags, mode]) 打开一个文件,并且设置需要的打开选项,mode参数是可选的
39 os.openpty() 打开一个新的伪终端对。返回 pty 和 tty的文件描述符。
40 os.pathconf(path, name) 返回相关文件的系统配置信息。
41 os.pipe() 创建一个管道. 返回一对文件描述符(r, w) 分别为读和写
42 [os.popen(command, mode[, bufsize]]) 从一个 command 打开一个管道
43 os.read(fd, n) 从文件描述符 fd 中读取最多 n 个字节,返回包含读取字节的字符串,文件描述符 fd对应文件已达到结尾, 返回一个空字符串。
44 os.readlink(path) 返回软链接所指向的文件
45 os.remove(path) 删除路径为path的文件。如果path 是一个文件夹,将抛出OSError; 查看下面的rmdir()删除一个 directory。
46 os.removedirs(path) 递归删除目录。
47 os.rename(src, dst) 重命名文件或目录,从 src 到 dst
48 os.renames(old, new) 递归地对目录进行更名,也可以对文件进行更名。
49 os.rmdir(path) 删除path指定的空目录,如果目录非空,则抛出一个OSError异常。
50 os.stat(path) 获取path指定的路径的信息,功能等同于C API中的stat()系统调用。
51 [os.stat_float_times(newvalue]) 决定stat_result是否以float对象显示时间戳
52 os.statvfs(path) 获取指定路径的文件系统统计信息
53 os.symlink(src, dst) 创建一个软链接
54 os.tcgetpgrp(fd) 返回与终端fd(一个由os.open()返回的打开的文件描述符)关联的进程组
55 os.tcsetpgrp(fd, pg) 设置与终端fd(一个由os.open()返回的打开的文件描述符)关联的进程组为pg。
56 [os.tempnam(dir[, prefix]]) 返回唯一的路径名用于创建临时文件。
57 os.tmpfile() 返回一个打开的模式为(w+b)的文件对象 .这文件对象没有文件夹入口,没有文件描述符,将会自动删除。
58 os.tmpnam() 为创建一个临时文件返回一个唯一的路径
59 os.ttyname(fd) 返回一个字符串,它表示与文件描述符fd 关联的终端设备。如果fd 没有与终端设备关联,则引发一个异常。
60 os.unlink(path) 删除文件
61 os.utime(path, times) 返回指定的path文件的访问和修改的时间。
62 [os.walk(top[, topdown=True[, onerror=None[, followlinks=False]]])](https://www.runoob.com/python/os-walk.html) 输出在文件夹中的文件名通过在树中游走,向上或者向下。
63 os.write(fd, str) 写入字符串到文件描述符 fd中. 返回实际写入的字符串长度
64 os.path 模块 获取文件的属性信息。
Python 内置函数
内置函数
abs() divmod() input() open() staticmethod()
all() enumerate() int() ord() str()
any() eval() isinstance() pow() sum()
basestring() execfile() issubclass() print() super()
bin() file() iter() property() tuple()
bool() filter() len() range() type()
bytearray() float() list() raw_input() unichr()
callable() format() locals() reduce() unicode()
chr() frozenset() long() reload() vars()
classmethod() getattr() map() repr() xrange()
cmp() globals() max() reverse() zip()
compile() hasattr() memoryview() round() import()
complex() hash() min() set()
delattr() help() next() setattr()
dict() hex() object() slice()
dir() id() oct() sorted() exec 内置表达式



Python高级

创建类 [面向对象]
class Employee:
    '所有员工的基本类型'
    empCount = 0

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
        Employee.empCount += 1

    def displayCount(self):
        print ("Total Employee %d" % Employee.empCount)
    def displayEmployee(self):
        print ("Name : ", self.name,  ", Salary: ", self.salary)

------------------------------------------
class Employee:  
    # 定义一个名为 Employee 的新类
    '所有员工的基本类型'  
    # 类的文档字符串,描述类的用途

    empCount = 0  
    # 类变量,用于跟踪类实例的数量

    def __init__(self, name, salary):  
        # 类的构造函数,用于创建类的新实例
        self.name = name  
        # 将参数 name 的值赋给实例变量 name
        self.salary = salary 
        # 将参数 salary 的值赋给实例变量 salary
        Employee.empCount += 1  
        # 每创建一个实例,类变量 empCount 的值就增加 1

    def displayCount(self):  
        # 类的方法,用于显示当前员工数量
        print("Total Employee %d" % Employee.empCount)              # 打印员工总数

    def displayEmployee(self):  
        # 类的方法,用于显示员工信息
        print("Name : ", self.name, ", Salary: ", self.salary)  
        # 打印员工的姓名和薪水
  • empCount 变量是一个类变量,它的值将在这个类的所有实例之间共享。你可以在内部类或外部类使用 Employee.empCount 访问。
  • 第一种方法__init__()方法是一种特殊的方法,被称为类的构造函数或初始化方法,当创建了这个类的实例时就会调用该方法
  • self 代表类的实例,self 在定义类的方法时是必须有的,虽然在调用时不必传入相应的参数。
创建实例对象

实例化类其他编程语言中一般用关键字 new,但是在 Python 中并没有这个关键字,类的实例化类似函数调用方式。以下使用类的名称 Employee 来实例化,并通过 init 方法接收参数

# 其余的都在上面的代码中
emp1 = Employee("Zara", 2000)
emp2 = Employee("Manni", 5000)
访问属性

您可以使用点号 . 来访问对象的属性。使用如下类的名称访问类变量:

emp1.displayEmployee()
emp2.displayEmployee()
print "Total Employee %d" % Employee.empCount
----------------------------------
Name :  Zara , Salary:  2000
Name :  Manni , Salary:  5000
Total Employee:2

你可以添加,删除,修改类的属性,如下所示:

emp1.age = 7  # 添加一个 'age' 属性
emp1.age = 8  # 修改 'age' 属性
del emp1.age  # 删除 'age' 属性

你也可以使用以下函数的方式来访问属性:

  • getattr(obj, name[, default]) : 访问对象的属性。
  • hasattr(obj,name) : 检查是否存在一个属性。
  • setattr(obj,name,value) : 设置一个属性。如果属性不存在,会创建一个新属性。
  • delattr(obj, name) : 删除属性。

hasattr(emp1, ‘age’) # 如果存在 ‘age’ 属性返回 True。
getattr(emp1, ‘age’) # 返回 ‘age’ 属性的值
setattr(emp1, ‘age’, 8) # 添加属性 ‘age’ 值为 8
delattr(emp1, ‘age’) # 删除属性 ‘age’

emp1.displayEmployee()
emp2.displayEmployee()
emp1.age = 7 # 添加一个 'age' 属性
print("姓名:%s" %emp1.name, "年龄:%d" %emp1.age)
print ("总计:%d" %Employee.empCount)
-----------------------------------
Name :  Zara , Salary:  2000
Name :  Manni , Salary:  5000
姓名:Zara 年龄:7
总计:2
python对象销毁(垃圾回收)

Python 使用了引用计数这一简单技术来跟踪和回收垃圾。在 Python 内部记录着所有使用中的对象各有多少引用。一个内部跟踪变量,称为一个引用计数器。当对象被创建时,就创建了一个引用计数,当这个对象不再需要时, 也就是说, 这个对象的引用计数变为0 时, 它被垃圾回收。但是回收不是”立即”的, 由解释器在适当的时机,将垃圾对象占用的内存空间回收。

a = 40      # 创建对象  <40>
b = a       # 增加引用, <40> 的计数
c = [b]     # 增加引用.  <40> 的计数

del a       # 减少引用 <40> 的计数
b = 100     # 减少引用 <40> 的计数
c[0] = -1   # 减少引用 <40> 的计数

垃圾回收机制不仅针对引用计数为0的对象,同样也可以处理循环引用的情况。循环引用指的是,两个对象相互引用,但是没有其他变量引用他们。这种情况下,仅使用引用计数是不够的。Python 的垃圾收集器实际上是一个引用计数器和一个循环垃圾收集器。作为引用计数的补充, 垃圾收集器也会留心被分配的总量很大(即未通过引用计数销毁的那些)的对象。 在这种情况下, 解释器会暂停下来, 试图清理所有未引用的循环。

类的继承

面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。通过继承创建的新类称为子类派生类,被继承的类称为基类父类超类

继承语法

class 派生类名(基类名)
    ...

在python中继承中的一些特点:

  • 1、如果在子类中需要父类的构造方法就需要显式的调用父类的构造方法,或者不重写父类的构造方法。详细说明可查看: python 子类继承父类构造函数说明
  • 2、在调用基类的方法时,需要加上基类的类名前缀,且需要带上 self 参数变量。区别在于类中调用普通函数时并不需要带上 self 参数
  • 3、Python 总是首先查找对应类型的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找。(先在本类中查找调用的方法,找不到才去基类中找)。

如果在继承元组中列了一个以上的类,那么它就被称作”多重继承” 。

语法:

派生类的声明,与他们的父类类似,继承的基类列表跟在类名之后
如下所示:

class SubClassName (ParentClass1[, ParentClass2, ...]):
    ...
方法重写

如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法:

class Parent:
    def myMethod(self):
        print ('调用父类方法')
class Child(Parent):
    def myMethod(self):
        print ('调用子类方法')
c = Child()
c.myMethod()
类属性与方法

类的私有属性

“_ _” → __private_attrs 两个下划线开头,声明属性为私有,不能在外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs

类的方法

在类的内部,使用def关键字可以为类定义一个方法,与一般函数定义不同,类方法必须包含参数self,且为第一个参数

类的私有方法

__private_method:两个下划线开头,声明该方法为私有方法,不能在类的外部调用。
在类的内部调用self.__private_methods

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
class JustCounter:
    __secretCount = 0  # 私有变量
    publicCount = 0    # 公开变量
 
    def count(self):
        self.__secretCount += 1
        self.publicCount += 1
        print self.__secretCount
 
counter = JustCounter()
counter.count()
counter.count()
print counter.publicCount
print counter.__secretCount  # 报错,实例不能访问私有变量
----------------------------------------------------
1
2
2
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    print counter.__secretCount  # 报错,实例不能访问私有变量
AttributeError: JustCounter instance has no attribute '__secretCount'

Python不允许实例化的类访问私有数据,但你可以使用 object._className__attrName对象名._类名__私有属性名 )访问属性,参考以下实例:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

class Runoob:
    __site = "www.runoob.com"

runoob = Runoob()
print runoob._Runoob__site

执行以上代码,执行结果如下:

www.runoob.com
下划线、双下划线、头尾双下划线说明:
  • _ _ foo _ _: 定义的是特殊方法,一般是系统定义名字 ,类似 init() 之类的。
  • _ foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
  • _ _ foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。

正则表达

re.match() 与 re.search()的区别

在 Python 的正则表达式模块 re 中,re.match()re.search() 都用于在字符串中查找正则表达式的匹配项,但它们在查找匹配项的方式上有所不同。

re.match() 只在字符串的开始处进行匹配。如果字符串的开头与正则表达式不匹配,re.match()将返回None

import re
# 使用 re.search()
string = "hello world"
pattern = "hello"
match = re.match(pattern, string)
if match:
    print(f"匹配成功: {match.group()}")
else:
    print("匹配失败")
----------------------
匹配成功: hello

re.search() 会扫描整个字符串,直到找到第一个匹配正则表达式的子串。它在整个字符串中查找匹配项,而不仅仅是在字符串的开始处。

import re
# 使用 re.search()
string = "world hello"
pattern = "hello"

search = re.search(pattern, string)

if search:
    print(f"Match found: {search.group()}")
else:
    print("No match found.")
-------------------------------
Match found: hello
检索和替换

Python 的 re 模块提供了re.sub用于替换字符串中的匹配项。

re.sub(pattern, repl, string, count=0, flags=0)

参数:

  • pattern : 正则中的模式字符串。
  • repl : 替换的字符串,也可为一个函数。
  • string : 要被查找替换的原始字符串。
  • count : 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。

代码分析

import re

phone = "2004-959-559 # 这是一个国外电话号码"

# 删除字符串中的 Python注释 
num = re.sub(r'#.*$', "", phone)
print("电话号码是: ", num)

# 删除非数字(-)的字符串 
num = re.sub(r'\D', "", phone)
print("电话号码是 : ", num)

第一部分:删除字符串中的注释

num = re.sub(r'#.*$', "", phone)
  • re.sub() 是 re 模块中的函数,用于替换字符串中的匹配项。

  • r'#.*$'
    

    是正则表达式模式:

    • # 表示匹配字符 #
    • .* 表示匹配任意字符(.)任意次数(*)。
    • $ 表示匹配字符串的末尾。
  • "" 是替换字符串,这里是一个空字符串,表示匹配到的部分将被删除。

  • phone 是要被处理的原始字符串。

这个正则表达式 r'#.*$' 的意思是:从 # 字符开始,匹配直到字符串末尾的所有字符。因此,该行代码将删除字符串 phone 中从 # 开始到字符串末尾的所有内容。

执行结果:

复制

电话号码是:  2004-959-559 

第二部分:删除非数字字符

num = re.sub(r'\D', "", phone)
  • re.sub() 同上,用于替换字符串中的匹配项。

  • r'\D'
    

    是正则表达式模式:

    • \D 表示匹配任意非数字字符。
  • "" 同上,是一个空字符串,表示匹配到的非数字字符将被删除。

  • phone 同上,是要被处理的原始字符串。

这个正则表达式 r'\D' 的意思是:匹配字符串中所有的非数字字符。因此,该行代码将删除字符串 phone 中所有非数字字符。

电话号码是 :  2004959559 

总结

这段代码展示了如何使用 re.sub() 函数结合正则表达式来清理和格式化字符串。第一个例子中,我们移除了字符串末尾的注释。第二个例子中,我们移除了电话号码中的所有非数字字符,从而得到一个仅包含数字的字符串。这些操作在数据清洗和预处理中非常有用。

import re
string = "hello world"
pattern = "hello"
match = re.match(pattern, string)
if match:
    print(f"匹配成功: {match.group()}")
else:
    print("匹配失败")
print('-----------------')
import re
phone = "2004-959-559 # 这是一个国外电话号码"
num = re.sub("#.*$","",phone)
print("电话号码是: ", num)
num2 = re.sub(r'\D',"",phone)
print("电话号码是: ", num2)
-----------------------------------------
电话号码是:  2004-959-559 
电话号码是:  2004959559

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

print(f"匹配成功: {match.group()}")

f-string 是 Python 3.6 及以上版本中引入的一种新的字符串格式化方法,它允许你在字符串字面量中嵌入表达式,并在运行时动态地进行求值和格式化。

  • f 告诉 Python 这是一个格式化字符串。
  • {match.group()} 是一个表达式,它将被求值,并且其结果将插入到字符串中相应的位置。
findall

在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果有多个匹配模式,则返回元组列表,如果没有找到匹配的,则返回空列表。

注意: match 和 search 是匹配一次,findall 是匹配所有。

语法格式为:

findall(string[, pos[, endpos]])

参数:

  • string : 待匹配的字符串。
  • pos : 可选参数,指定字符串的起始位置,默认为 0。
  • endpos : 可选参数,指定字符串的结束位置,默认为字符串的长度。

查找字符串中的所有数字:

# -*- coding:UTF8 -*-
 
import re
# 这里您编译了一个正则表达式模式,该模式 r'\d+' 匹配一个或多个数字。
# \d 表示数字 [0-9],+ 表示一个或多个。
pattern = re.compile(r'\d+')   # 查找数字
# 这里您在字符串 'runoob 123 google 456' 中查找所有匹配的数字。
# 由于字符串中有两个数字序列 '123' 和 '456',所以返回一个列表 ['123', '456']
result1 = pattern.findall('runoob 123 google 456')
# 在这里,您同样查找数字,但是指定了搜索的起始位置 0 和结束位置 10。
# 这意味着搜索只会在字符串的前10个字符内进行。在这个范围内,有两个数字序列 '88' 和 '12',所以返回列表 ['88', '12']
result2 = pattern.findall('run88oob123google456', 0, 10)
 
print(result1)
print(result2)

输出结果:

['123', '456']
['88', '12']
re.finditer

和 findall 类似,在字符串中找到正则表达式所匹配的所有子串,并把它们作为一个迭代器返回。

re.finditer(pattern, string, flags=0)
参数 描述
pattern 匹配的正则表达式
string 要匹配的字符串。
flags 标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志
# -*- coding: UTF-8 -*-
 
import re
 
it = re.finditer(r"\d+","12a32bc43jf3") 
for match in it:
    print(match)          # 打印匹配对象的描述
    print("==========")
    print(match.group())  # 打印匹配到的字符串

下面是具体的区别:

  • 当您执行 print(match) 时,您实际上是在打印匹配对象的内存地址或者一个表示匹配对象的字符串。这个输出通常用于调试,以便快速查看对象的类型和标识。

    print(match.group()) 是用来获取匹配对象中的实际匹配到的字符串。group() 方法返回正则表达式匹配到的整个字符串。

  • print(match)

    • 打印的是匹配对象的描述,通常包括对象的类型和内存地址。
    • 对于调试目的,这有助于了解对象的身份,但不显示匹配到的文本。
  • print(match.group())

    • 打印的是通过正则表达式匹配到的具体字符串。
    • 这是获取匹配文本的实际内容的方法。
re.split

split方法按照能够匹配的子串将字符串分割后返回列表

import re
# 定义一个包含单词和数字的字符串
text = "单词1单词2单词3单词4"
# 使用 re.split() 按照数字进行分割,但不保留数字
# (?:\d+) 匹配一个或多个数字,但不会捕获匹配的数字
result = re.split(r'(?:\d+)', text)
print(result)
正则表达式模式

模式字符串使用特殊的语法来表示一个正则表达式:
字母和数字表示他们自身。一个正则表达式模式中的字母和数字匹配同样的字符串。
多数字母和数字前加一个反斜杠时会拥有不同的含义。
标点符号只有被转义时才匹配自身,否则它们表示特殊的含义。
反斜杠本身需要使用反斜杠转义。
由于正则表达式通常都包含反斜杠,所以你最好使用原始字符串来表示它们。模式元素(如 r’\t’,等价于 ‘\t’)匹配相应的特殊字符。
下表列出了正则表达式模式语法中的特殊元素。如果你使用模式的同时提供了可选的标志参数,某些模式元素的含义会改变。

模式 描述
^ 匹配字符串的开头
$ 匹配字符串的末尾。
. 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[…] 用来表示一组字符,单独列出:[amk] 匹配 ‘a’,’m’或’k’
[^…] 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。
re* 匹配0个或多个的表达式。
re+ 匹配1个或多个的表达式。
re? 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
re{ n} 精确匹配 n 个前面表达式。例如, o{2} 不能匹配 “Bob” 中的 “o”,但是能匹配 “food” 中的两个 o。
re{ n,} 匹配 n 个前面表达式。例如, o{2,} 不能匹配”Bob”中的”o”,但能匹配 “foooood”中的所有 o。”o{1,}” 等价于 “o+”。”o{0,}” 则等价于 “o*”。
re{ n, m} 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a| b 匹配a或b
(re) 对正则表达式分组并记住匹配的文本
(?imx) 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。
(?-imx) 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。
(?: re) 类似 (…), 但是不表示一个组
(?imx: re) 在括号中使用i, m, 或 x 可选标志
(?-imx: re) 在括号中不使用i, m, 或 x 可选标志
(?#…) 注释.
(?= re) 前向肯定界定符。如果所含正则表达式,以 … 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
(?! re) 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功
(?> re) 匹配的独立模式,省去回溯。
\w 匹配字母数字及下划线
\W 匹配非字母数字及下划线
\s 匹配任意空白字符,等价于 **[ \t\n\r\f]**。
\S 匹配任意非空字符
\d 匹配任意数字,等价于 [0-9].
\D 匹配任意非数字
\A 匹配字符串开始
\Z 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。
\z 匹配字符串结束
\G 匹配最后匹配完成的位置。
\b 匹配一个单词边界,也就是指单词和空格间的位置。例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
\B 匹配非单词边界。’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
\n, \t, 等. 匹配一个换行符。匹配一个制表符。等
\1…\9 匹配第n个分组的内容。
\10 匹配第n个分组的内容,如果它经匹配。否则指的是八进制字符码的表达式
正则表达式实例

字符匹配

实例 描述
python 匹配 “python”.

字符类

实例 描述
[Pp]ython 匹配 “Python” 或 “python”
rub[ye] 匹配 “ruby” 或 “rube”
[aeiou] 匹配中括号内的任意一个字母
[0-9] 匹配任何数字。类似于 [0123456789]
[a-z] 匹配任何小写字母
[A-Z] 匹配任何大写字母
[a-zA-Z0-9] 匹配任何字母及数字
[^aeiou] 除了aeiou字母以外的所有字符
[^0-9] 匹配除了数字外的字符
特殊字符类
实例 描述
. 匹配除 “\n” 之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用象 ‘[.\n]’ 的模式。
\d 匹配一个数字字符。等价于 [0-9]。
\D 匹配一个非数字字符。等价于 [^0-9]。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\w 匹配包括下划线的任何单词字符。等价于’[A-Za-z0-9_]’。
\W 匹配任何非单词字符。等价于 ‘[^A-Za-z0-9_]’。
request请求的用法

以下是使用 Python 标准库中的 urllib 模块进行 HTTP GET POST 请求的简化用法。

Get请求request.get()

import requests

# 简化的 GET 请求
def simple_get(url):
    response = requests.get(url)
    return response.text

# 使用 GET 请求
url = 'http://httpbin.org/get'
response_text = simple_get(url)
print(response_text)

Post请求request.post()

import requests

# 简化的 POST 请求
def simple_post(url, data):
    response = requests.post(url, data=data)
    return response.text

# 使用 POST 请求
url = 'http://httpbin.org/post'
data = {'key1': 'value1', 'key2': 'value2'}
response_text = simple_post(url, data)
print(response_text)
SMTP发送邮件
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header


# 设置QQ邮箱的SMTP服务器地址和端口
smtp_server = 'smtp.qq.com'
smtp_port = 465  # QQ邮箱SMTP服务器端口为465或587

# 设置发件人和收件人邮箱
sender_email = '390415032@qq.com'  # 发件人邮箱
receiver_email = 'recipient@example.com'  # 收件人邮箱
password = 'your_qq_email_auth_code'  # QQ邮箱授权码

# 设置邮件主题和内容
subject = 'Test Email from Python'
body = 'This is a test email sent by Python.'

# 创建MIMEText对象,设置邮件内容和编码
msg = MIMEText(body, 'plain', 'utf-8')
msg['From'] = Header(sender_email, 'utf-8')
msg['To'] = Header(receiver_email, 'utf-8')
msg['Subject'] = Header(subject, 'utf-8')

# 发送邮件
try:
    # 创建SMTP对象
    server = smtplib.SMTP_SSL(smtp_server, smtp_port)
    # 登录SMTP服务器
    server.login(sender_email, password)
    # 发送邮件
    server.sendmail(sender_email, [receiver_email], msg.as_string())
    print('Email sent successfully!')
except smtplib.SMTPException as e:
    print('Error: unable to send email. ', e)
finally:
    # 关闭服务器连接
    server.quit()
Python 多线程

Python中使用线程有两种方式:函数或者用类来包装线程对象。

函数式:调用thread模块中的start_new_thread()函数来产生新线程。语法如下:

thread.start_new_thread ( function, args[, kwargs] )

参数说明:

  • function - 线程函数。
  • args - 传递给线程函数的参数,他必须是个tuple类型。
  • kwargs - 可选参数。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
import thread
import time
 
# 为线程定义一个函数
def print_time( threadName, delay):
   count = 0
   while count < 5:
      time.sleep(delay)
      count += 1
      print "%s: %s" % ( threadName, time.ctime(time.time()) )
 
# 创建两个线程
try:
   thread.start_new_thread( print_time, ("Thread-1", 2, ) )
   thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
   print "Error: unable to start thread"
 
while 1:
   pass

-------------------------------------------------------
Thread-1: Thu Jan 22 15:42:17 2009
Thread-1: Thu Jan 22 15:42:19 2009
Thread-2: Thu Jan 22 15:42:19 2009
Thread-1: Thu Jan 22 15:42:21 2009
Thread-2: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:25 2009
Thread-2: Thu Jan 22 15:42:27 2009
Thread-2: Thu Jan 22 15:42:31 2009
Thread-2: Thu Jan 22 15:42:35 2009

线程的结束一般依靠线程函数的自然结束;也可以在线程函数中调用thread.exit(),他抛出SystemExit exception,达到退出线程的目的。

线程模块

Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。

threading 模块提供的其他方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

  • run(): 用以表示线程活动的方法。
  • start(): 启动线程活动。
  • join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
  • isAlive(): 返回线程是否活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。
使用Threading模块创建线程

使用Threading模块创建线程,直接从threading.Thread继承
然后重写__init__方法和run方法:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
import threading
import time
 
exitFlag = 0
 
class myThread (threading.Thread):   #继承父类threading.Thread
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):                   #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数 
        print "Starting " + self.name
        print_time(self.name, self.counter, 5)
        print "Exiting " + self.name
 
def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            (threading.Thread).exit()
        time.sleep(delay)
        print "%s: %s" % (threadName, time.ctime(time.time()))
        counter -= 1
 
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
 
# 开启线程
thread1.start()
thread2.start()
 
print "Exiting Main Thread"

------------------------------------------
Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Thu Mar 21 09:10:03 2013
Thread-1: Thu Mar 21 09:10:04 2013
Thread-2: Thu Mar 21 09:10:04 2013
Thread-1: Thu Mar 21 09:10:05 2013
Thread-1: Thu Mar 21 09:10:06 2013
Thread-2: Thu Mar 21 09:10:06 2013
Thread-1: Thu Mar 21 09:10:07 2013
Exiting Thread-1
Thread-2: Thu Mar 21 09:10:08 2013
Thread-2: Thu Mar 21 09:10:10 2013
Thread-2: Thu Mar 21 09:10:12 2013
Exiting Thread-2
线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。

使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。如下:

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。

考虑这样一种情况:一个列表里所有元素都是0,线程”set”从后向前把所有元素改成1,而线程”print”负责从前往后读取列表并打印。

那么,可能线程”set”开始改的时候,线程”print”便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。

锁有两种状态——锁定和未锁定。每当一个线程比如”set”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”set”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”set”继续。

经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。

#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
import threading
import time
 
class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print "Starting " + self.name
       # 获得锁,成功获得锁定后返回True
       # 可选的timeout参数不填时将一直阻塞直到获得锁定
       # 否则超时后将返回False
        threadLock.acquire()
        print_time(self.name, self.counter, 3)
        # 释放锁
        threadLock.release()
 
def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print "%s: %s" % (threadName, time.ctime(time.time()))
        counter -= 1
 
threadLock = threading.Lock()
threads = []
 
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
 
# 开启新线程
thread1.start()
thread2.start()
 
# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)
 
# 等待所有线程完成
for t in threads:
    t.join()
print "Exiting Main Thread"
线程优先级队列( Queue)

Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。

Queue模块中的常用方法:

  • Queue.qsize() 返回队列的大小
  • Queue.empty() 如果队列为空,返回True,反之False
  • Queue.full() 如果队列满了,返回True,反之False
  • Queue.full 与 maxsize 大小对应
  • Queue.get([block[, timeout]])获取队列,timeout等待时间
  • Queue.get_nowait() 相当Queue.get(False)
  • Queue.put(item, block=True, timeout=None) 写入队列,timeout等待时间
  • Queue.put_nowait(item) 相当 Queue.put(item, False)
  • Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
  • Queue.join() 实际上意味着等到队列为空,再执行别的操作
#!/usr/bin/python
# -*- coding: UTF-8 -*-
 
import Queue
import threading
import time
 
exitFlag = 0
 
class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print "Starting " + self.name
        process_data(self.name, self.q)
        print "Exiting " + self.name
 
def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print "%s processing %s" % (threadName, data)
        else:
            queueLock.release()
        time.sleep(1)
 
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
threads = []
threadID = 1
 
# 创建新线程
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1
 
# 填充队列
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()
 
# 等待队列清空
while not workQueue.empty():
    pass
 
# 通知线程是时候退出
exitFlag = 1
 
# 等待所有线程完成
for t in threads:
    t.join()
print "Exiting Main Thread"
---------------------------------------------------
Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-1 processing One
Thread-2 processing Two
Thread-3 processing Three
Thread-1 processing Four
Thread-2 processing Five
Exiting Thread-3
Exiting Thread-1
Exiting Thread-2
Exiting Main Thread

Python XML解析

XML 指可扩展标记语言(eXtensible Markup Language);XML 被设计用来传输和存储数据。XML 是一套定义语义标记的规则,这些标记将文档分成许多部件并对这些部件加以标识。它也是元标记语言,即定义了用于定义其他与特定领域有关的、语义的、结构化的标记语言的句法语言。常见的 XML 编程接口有 DOM 和 SAX,这两种接口处理 XML 文件的方式不同,当然使用场合也不同。
Python 有三种方法解析 XML,SAX,DOM,以及 ElementTree:

1.SAX (simple API for XML )

Python 标准库包含 SAX 解析器,SAX 用事件驱动模型,通过在解析XML的过程中触发一个个的事件并调用用户定义的回调函数来处理XML文件。

2.DOM(Document Object Model)

将 XML 数据在内存中解析成一个树,通过对树的操作来操作XML。

3.ElementTree(元素树)

ElementTree就像一个轻量级的DOM,具有方便友好的API。代码可用性好,速度快,消耗内存少。

注:因DOM需要将XML数据映射到内存中的树,一是比较慢,二是比较耗内存,而SAX流式读取XML文件,比较快,占用内存少,但需要用户实现回调函数(handler)。

XML解析一下

import xml.sax
from lxml import etree

# 确保文件路径正确
xml_file_path = 'movies.xml'

# 使用 lxml 创建一个 XML 解析器
parser = etree.XMLParser()

# 解析 XML 文件
try:
    tree = etree.parse(xml_file_path, parser)
    # 遍历所有元素并打印
    for element in tree.iter():
        print(f"Element tag: {element.tag}, text: {element.text.strip() if element.text else ''}")
except etree.XMLSyntaxError as e:
    print(f"XML 解析错误: {e}")
movies.xml
<collection shelf="New Arrivals">
<movie title="Enemy Behind">
   <type>War, Thriller</type>
   <format>DVD</format>
   <year>2003</year>
   <rating>PG</rating>
   <stars>10</stars>
   <description>Talk about a US-Japan war</description>
</movie>
<movie title="Transformers">
   <type>Anime, Science Fiction</type>
   <format>DVD</format>
   <year>1989</year>
   <rating>R</rating>
   <stars>8</stars>
   <description>A schientific fiction</description>
</movie>
   <movie title="Trigun">
   <type>Anime, Action</type>
   <format>DVD</format>
   <episodes>4</episodes>
   <rating>PG</rating>
   <stars>10</stars>
   <description>Vash the Stampede!</description>
</movie>
<movie title="Ishtar">
   <type>Comedy</type>
   <format>VHS</format>
   <rating>PG</rating>
   <stars>2</stars>
   <description>Viewable boredom</description>
</movie>
</collection>
阅读全文

LlamaIndex

2025/2/25

LlamaIndex喂食给AI并进化升级

将您的企业数据转化为可用于生产的LLM应用程序

LLM 提供人与数据之间的自然语言接口。LLM 预先训练过大量公开数据,但它们并非基于您的数据进行训练。您的数据可能是私有的,也可能是特定于您要解决的问题的数据。它隐藏在 AP1后面、SQL 数据库中,或隐藏在PDF 和幻灯片中。上下文增强使 LLM 可以使用您的数据来解决手头的问题。Llamalndex 提供构建任何上下文增强用例的工具,从原型到生产我们的工具允许您提取、解析、索引和处理您的数据,并快速实施将数据访问与 LLM 提示相结合的复杂查询工作流。
上下文增强最流行的示例是检索增强生成RAG,它在推理时将上下文与ILLM 相结合。

LlamaIndex 是一个将大语言模型(Large Language Models, LLMs,后简称大模型)和外部数据连接在一起的工具。大模型依靠上下文学习(Context Learning)来推理知识,针对一个输入(或者是prompt),根据其输出结果。因此Prompt的质量很大程度上决定了输出结果的质量,因此提示工程(Prompt engineering)现在也很受欢迎。目前大模型的输入输出长度因模型结构、显卡算力等因素影响,都有一个长度限制(以Token为单位,ChatGPT限制长度为4k个,GPT-4是32k等,Claude最新版有个100k的)。当我们外部知识的内容超过这个长度时,就无法同时将有效的信息传递给大模型。因此就诞生了 LlamaIndex 等项目。

假设有一个10w的外部数据,我们的原始输入Prompt长度为100,长度限制为4k,通过查询-检索的方式,我们能将最有效的信息提取集中在这4k的长度中,与Prompt一起送给大模型,从而让大模型得到更多的信息。此外,还能通过多轮对话的方式不断提纯外部数据,达到在有限的输入长度限制下,传达更多的信息给大模型。这部分知识可参考

LLamaIndex的任务是通过查询、检索的方式挖掘外部数据的信息,并将其传递给大模型,因此其主要由x部分组成:

  1. 数据连接。首先将数据能读取进来,这样才能挖掘。

  2. 索引构建。要查询外部数据,就必须先构建可以查询的索引,llamdaIndex将数据存储在Node中,并基于Node构建索引。索引类型包括向量索引列表索引树形索引等;

  3. 查询接口。有了索引,就必须提供查询索引的接口。通过这些接口用户可以与不同的 大模型进行对话,也能自定义需要的Prompt组合方式。查询接口会完成 检索+对话的功能,即先基于索引进行检索,再将检索结果和之前的输入Prompt进行(自定义)组合形成新的扩充Prompt,对话大模型并拿到结果进行解析。

1 数据连接器(Data Connectors)

数据连接器,读取文档的工具,最简单的就是读取本地文件。 LLamaIndex 的数据连接器包括

  • 本地文件、Notion、Google 文档、Slack、Discord

具体可参考Data Connectors。

2 索引结构(Index Structures)

LlamaIndex 的核心其实就是 索引结构的集合,用户可以使用索引结构或基于这些索引结构自行建图。

2.1 索引如何工作

两个概念:

  • Node(节点):即一段文本(Chunk of Text),LlamaIndex读取文档(documents)对象,并将其解析/划分(parse/chunk)成 Node 节点对象,构建起索引。
  • Response Synthesis(回复合成):LlamaIndex 进行检索节点并响应回复合成,不同的模式有不同的响应模式(比如向量查询、树形查询就不同),合成不同的扩充Prompt。

索引方式包括

  • List Index:Node顺序存储,可用关键字过滤Node
  • Vector Store Index:每个Node一个向量,查询的时候取top-k相似
  • Tree Index:树形Node,从树根向叶子查询,可单边查询,或者双边查询合并。
  • **Keyword Table Index**:每个Node有很多个Keywords链接,通过查Keyword能查询对应Node。

不同的索引方式决定了Query选择Node方式的不同。

回复合成方式包括:

  • 创建并提纯(Create and Refine),即线性依次迭代;

  • 树形总结(Tree Summarize):自底向上,两两合并,最终合并成一个回复。

3 查询接口(Query Inference)

3.1 LlamaIndex 使用模板

LlamaIndex 常用使用模版:

  1. 读取文档 (手动添加or通过Loader自动添加);
  2. 将文档解析为Nodes;
  3. 构建索引(从文档or从Nodes,如果从文档,则对应函数内部会完成第2步的Node解析)
  4. [可选,进阶] 在其他索引上构建索引,即多级索引结构
  5. 查询索引并对话大模型

LangChain vs LlamaIndex

综合构建用于生产的高性能RAG程序 就用LlamaIndex
LlamaIndex官网参考:https://www.llamaindex.ai/
python文档参考:[LlamaIndex - LlamaIndex] (https://docs.llamaindex.ai/en/stable/)

LLM官网最下方的入门项目也需要学习
入门项目

RAG Work Flow

构建RAG管道—加载数据(镊取)

在您选择的 LLM 可以处理您的数据之前,您首先需要处理数据并加载数据。这与 ML 领域的数据清理/特征工程管道或传统数据设置中的 ETL 管道有相似之处。

此引入管道通常包括三个主要阶段:

  1. 加载数据
  2. 转换数据
  3. 索引和存储数据

我们将在后面的章节中介绍索引 / 存储。在本指南中,我们将主要讨论 loader 和 transformations。

装载机

在您选择的 LLM 可以处理您的数据之前,您需要加载它。LlamaIndex 执行此作的方式是通过数据连接器(也称为 .Data Connector 从不同的数据源摄取数据并将数据格式化为对象。A 是有关该数据的数据(当前为文本,将来为图像音频)和元数据的集合。Reader Document Document

使用 SimpleDirectoryReader 加载

最容易使用的阅读器是我们的 SimpleDirectoryReader,它从给定目录中的每个文件创建文档。它内置于 LlamaIndex 中,可以读取多种格式,包括 Markdown、PDF、Word 文档、PowerPoint 幻灯片、图像、音频和视频

from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader("./data").load_data()

★ ★ ★ ★ 更多教程请看官方接口文档 Loading Data (Ingestion) - LlamaIndex ★ ★ ★ ★

Starter Tutorial - LlamaIndex + 欢迎使用 Colaboratory - Colab = 在线使用代码

RAG

RAG,也称为检索增强生成,是利用个人或私域数据增强 LLM 的一种范式。通常,它包含两个阶段:

  1. 索引

    构建知识库。

  2. 查询

    从知识库检索相关上下文信息,以辅助 LLM 回答问题。

LlamaIndex 提供了工具包帮助开发者极其便捷地完成这两个阶段的工作。

索引阶段

LlamaIndex 通过提供 Data connectors(数据连接器) 和 Indexes (索引) 帮助开发者构建知识库。

该阶段会用到如下工具或组件:

  • Data connectors

    数据连接器。它负责将来自不同数据源的不同格式的数据注入,并转换为 LlamaIndex 支持的文档(Document)表现形式,其中包含了文本和元数据。

  • Documents / Nodes

    Document是 LlamaIndex 中容器的概念,它可以包含任何数据源,包括,PDF文档,API响应,或来自数据库的数据。

    Node是 LlamaIndex 中数据的最小单元,代表了一个 Document的分块。它还包含了元数据,以及与其他Node的关系信息。这使得更精确的检索操作成为可能。

  • Data Indexes

    LlamaIndex 提供便利的工具,帮助开发者为注入的数据建立索引,使得未来的检索简单而高效。

    最常用的索引是向量存储索引 - VectorStoreIndex

查询阶段

在查询阶段,RAG 管道根据的用户查询,检索最相关的上下文,并将其与查询一起,传递给 LLM,以合成响应。这使 LLM 能够获得不在其原始训练数据中的最新知识,同时也减少了虚构内容。该阶段的关键挑战在于检索、编排和基于知识库的推理。

LlamaIndex 提供可组合的模块,帮助开发者构建和集成 RAG 管道,用于问答、聊天机器人或作为代理的一部分。这些构建块可以根据排名偏好进行定制,并组合起来,以结构化的方式基于多个知识库进行推理。

该阶段的构建块包括:

  • Retrievers

    检索器。它定义如何高效地从知识库,基于查询,检索相关上下文信息。

  • Node Postprocessors

    Node后处理器。它对一系列文档节点(Node)实施转换,过滤,或排名。

  • Response Synthesizers

    响应合成器。它基于用户的查询,和一组检索到的文本块(形成上下文),利用 LLM 生成响应。

RAG管道包括:

  • Query Engines

    查询引擎 - 端到端的管道,允许用户基于知识库,以自然语言提问,并获得回答,以及相关的上下文。

  • Chat Engines

    聊天引擎 - 端到端的管道,允许用户基于知识库进行对话(多次交互,会话历史)。

  • Agents

    代理。它是一种由 LLM 驱动的自动化决策器。代理可以像查询引擎或聊天引擎一样使用。主要区别在于,代理动态地决定最佳的动作序列,而不是遵循预定的逻辑。这为其提供了处理更复杂任务的额外灵活性。

LlamaIndex个性化配置

LlamaIndexRAG 过程提供了全面的配置支持,允许开发者对整个过程进行个性化设置。常见的配置场景包括:

  • 自定义文档分块
  • 自定义向量存储
  • 自定义检索
  • 指定 LLM
  • 指定响应模式
  • 指定流式响应

注,个性化配置主要通过 LlamaIndex 提供的 ServiceContext 类实现。

配置场景示例

接下来通过简明示例代码段展示 LlamaIndex 对各种配置场景的支持。

自定义文档分块
from llama_index import ServiceContext
service_context = ServiceContext.from_defaults(chunk_size=500)
自定义向量存储
import chromadb
from llama_index.vector_stores import ChromaVectorStore
from llama_index import StorageContext

chroma_client = chromadb.PersistentClient()
chroma_collection = chroma_client.create_collection("quickstart")
# 向量存储
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
自定义检索

自定义检索中,我们可以通过参数指定查询引擎(Query Engine)在检索时请求的相似文档数。

index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=5)
指定 LLM
# 指定大语言模型
service_context = ServiceContext.from_defaults(llm=OpenAI())
指定响应模式
query_engine = index.as_query_engine(response_mode='tree_summarize')
指定流式响应
# 流式响应
query_engine = index.as_query_engine(streaming=True)
完整实例

GitHubLlamaIndex-Tutorials/03_Customization/03_Customization.ipynb at main · sugarforever/LlamaIndex-Tutorials
Colab03_Customization.ipynb - Colab

请参考03_Customization.ipynb ,这是一个基于第1课的示例实现上述的所有个性化配置:

  1. 文档分块大小:500
  2. Chromadb作为向量存储
  3. 自定义检索文档数为5
  4. 指定大模型为OpenAI的模型
  5. 响应模式为 tree_summarize
  6. 问答实现流式响应

注,响应模式会在后续课程中详细介绍。


强大的数据连接器

开源教程LlamaIndex-Tutorials/04_Data_Connectors at main · sugarforever/LlamaIndex-Tutorials
演示实例04_Data_Connectors.ipynb - Colab

文档与节点

开源教程LlamaIndex-Tutorials/05_Documents_Nodes at main · sugarforever/LlamaIndex-Tutorials

LlamaPack 新手入门

LlamaPacks - LlamaIndex

阅读全文

洪哥面试题

2025/2/9

线程池的执行流程大致如下:

线程池:ThreadPoolExecutor
一开始new的时候没有 是空的。先当一个任务提交给线程池时,线程池首先检查当前运行的线程数是否达到核心线程数。如果没有达到核心线程数,线程池会创建一个新的线程来执行任务。如果已经达到核心线程数,线程池会将任务放入工作队列中等待执行。如果工作队列满了,并且当前运行的线程数小于最大线程数,线程池会创建新的线程来执行任务。如果工作队列满了,并且当前运行的线程数等于最大线程数,线程池会根据拒绝策略

  • 丢弃任务抛出异常
  • 丢弃任务不抛弃异常
  • 丢弃队列最前面的任务,然后重新提交被拒绝的任务、
  • 由主线程处理该任务来处理无法执行的任务。【线程池无法起到异步问题】
    • 问题:想继续异步且不丢弃任务怎么办?
    • 把这个业务先存到别的地方 ↓↓↓
  • 自定义拒绝策略 自己写实现类实现拒绝策略 可以先存到mysql到时候再慢慢搞

怎么确定核心线程数和最大线程数

核心线程数
  • CPU密集型任务:如果任务是CPU密集型的,即任务主要是进行计算而不是等待I/O操作,核心线程数通常设置为CPU核心数加1。这样可以确保CPU在忙于计算的同时,还有额外的线程来处理可能出现的临时高峰。【纯内存计算 不涉及到网络计算和io计算】【N+1】
    • 八个核 创建十个cpu 没意义 因为最多并发只是8,建议保持一致或者+1,减少加入队列和创建队列的开销
    • 先把其当成io密集 因为层级不一样 不断压测去逼近最理想值
  • I/O密集型任务:对于I/O密集型任务,由于线程在等待I/O操作时会阻塞,因此可以设置更多的核心线程数。一个常用的经验法则是核心线程数设置为CPU核心数的两倍。【线程数越多越好】【压测无限逼近取最合适的线程数】【2N+1】
最大线程数

需要一开始创建好线程等着访问来,如果 核心=最大,此时没有临时线程

创建线程有几种方式(必会)

1.继承 Thread类并重写 run 方法创建线程,实现简单但不可以继承其他类
2.实现 Runnable接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。 [无返回值]
3.实现 Callable接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。[有返回值]
4.使用线程池创建(使用java.util.concurrent.Executor接口)

  • 想获得线程池里的返回结果用什么?execute + submit
  • 线程有哪些状态? java线程有哪些状态?
  • 线程池有哪些状态?
// 1. 继承Thread类并重写run方法
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程运行中 - 继承Thread类");
    }
}

// 2. 实现Runnable接口并重写run方法
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行中 - 实现Runnable接口");
    }
}

// 3. 实现Callable接口并重写call方法
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("线程运行中 - 实现Callable接口");
        return "Callable线程返回结果";
    }
}

// 4. 使用线程池创建线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadCreationExamples {
    public static void main(String[] args) {
        // 继承Thread类创建线程
        Thread thread1 = new MyThread();
        thread1.start();

        // 实现Runnable接口创建线程
        Thread thread2 = new Thread(new MyRunnable());
        thread2.start();

        // 实现Callable接口创建线程
        MyCallable callable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        Thread thread3 = new Thread(futureTask);
        thread3.start();
        try {
            // 获取线程执行结果的返回值
            String result = futureTask.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 使用线程池创建线程
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new MyRunnable()); // 提交Runnable任务
        executor.submit(new MyCallable()); // 提交Callable任务
        executor.shutdown(); // 关闭线程池
    }
}

线程池哪些类型?通过JUC[包]的executes可以创建这四个类型的线程池

问题:为什么阿里巴巴不推荐JUC?有可能会出现OOM、资源浪费

  • 单线程线程池
  • 可缓存线程池/定长
  • 变长的线程池
  • 定时任务的线程池

java 线程池创建时核心参数(高薪常问)

七大核心参数解释
参数名 说明
corePoolSize 核心线程数,线程池常驻的线程数量,即使空闲也不会被销毁(除非 allowCoreThreadTimeOut = true
maximumPoolSize 最大线程数,线程池能容纳的最大线程数量(包括核心线程)
keepAliveTime 非核心(临时)线程的存活时间,超过这个时间,空闲的线程会被回收
unit 时间单位keepAliveTime 的时间单位,如:秒(TimeUnit.SECONDS
workQueue 任务(阻塞)队列,用于缓存提交但尚未执行的任务(如:ArrayBlockingQueueLinkedBlockingQueue
threadFactory 线程工厂,用于自定义线程创建(可以设置线程名称、是否为守护线程等)
handler 拒绝策略,当任务队列已满且线程数量达到最大线程数时的处理方式;(抛不抛异常…)

核心线程池大小、线程池创建线程的最大个数(核心+非核心[临时线程])、临时线程存活时间、时间单位、阻塞队列、线程工厂(指定线程池创建线程的命名)、拒绝策略
线程工厂可以设置创建的属性
守护线程:主线程(main)一天不死 守护线程不死 [同生共死]
非守护线程:new一个就是 [不是同生共死]

阻塞队列常用的队列

  1. ArrayBlockingQueue: 基于数组结构的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。创建时需要指定容量。【底层是数组 随机读写的 **时间复杂度O(1)**】
    • 开辟新空间创建新数组 把旧数组的数据迁移过去 new ArrayList为空 需要add才可以 扩容是+10 取1.5倍
    • 高并发不会超过某个值 数组不会涉及到扩容 性能会好一些【比较稳定能预估】
    • new的时候不用指定长度
  2. LinkedBlockingQueue: 基于链表结构的有界阻塞队列(如果不指定容量,则默认为Integer.MAX_VALUE,即视为无界)。按照先进先出的原则排序元素。【随机读写的 时间复杂度O(n) 随机读写快 查询慢 是通过二分查找定位到下标元素(通过下标访问数组和链表) 只会走一次二分查找】
    • 读中间的慢 读头尾快
    • 新增元素不涉及到数组的迁移
    • 一般情况下高并发推荐使用,因为队列高级数据结构(可以用数组和链表的实现 由于底层数据结构不同)的特性是先进先出,链表不涉及到数组的扩容 末尾的最快是O(1)【不稳定】
    • new的时候可指定长度是最大链表的长度
    • 不可指定长度 [有界队列&无界队列] → 可能产生JVM的OOM

线程池的应用要有实际的业务场景

  • 异步任务处理将任务提交到线程池异步执行,而不阻塞主线程

假设我们有一个电商平台,其中一个核心业务是处理用户订单。在订单处理过程中,我们需要执行以下任务:

  1. 验证订单信息(例如:检查库存、验证用户信息等)。
  2. 计算订单金额(包括商品价格、折扣、运费等)。
  3. 生成订单并保存到数据库。
  4. 发送订单确认邮件给用户。

由于这些任务相对独立,并且处理时间可能较长,我们希望在不影响用户操作的前提下异步执行它们。以下是使用线程池处理这些异步任务的模拟代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Order {
    // 订单信息
    private String orderId;
    private String userId;
    private double amount;

    // 构造函数、getter和setter省略
}

class OrderService {
    private ExecutorService executorService = Executors.newFixedThreadPool(10); // 假设线程池大小为10

    // 处理订单
    public void processOrder(Order order) {
        // 1. 验证订单信息
        executorService.submit(() -> {
            System.out.println("验证订单信息: " + order.getOrderId());
            // 假设验证成功
        });

        // 2. 计算订单金额
        executorService.submit(() -> {
            System.out.println("计算订单金额: " + order.getOrderId());
            // 假设计算成功,设置订单金额
            order.setAmount(100.0); // 示例金额
        });

        // 3. 生成订单并保存到数据库
        executorService.submit(() -> {
            System.out.println("生成订单并保存到数据库: " + order.getOrderId());
            // 假设保存成功
        });

        // 4. 发送订单确认邮件
        executorService.submit(() -> {
            System.out.println("发送订单确认邮件: " + order.getOrderId());
            // 假设邮件发送成功
        });
    }

    // 关闭线程池
    public void shutdown() {
        try {
            executorService.shutdown();
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
        }
    }
}

public class ThreadPoolApplication {
    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        Order order = new Order();
        order.setOrderId("ORDER12345");
        order.setUserId("USER12345");

        // 处理订单
        orderService.processOrder(order);

        // 假设主线程还有其他任务,这里模拟等待其他任务完成
        try {
            Thread.sleep(5000); // 等待5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        orderService.shutdown();
    }
}

在这个模拟场景中,我们创建了一个 OrderService 类,它包含一个线程池用于异步处理订单的各个步骤。当用户创建订单时,processOrder 方法会将订单处理的各个任务提交到线程池中异步执行。这样,主线程不会被阻塞,可以继续处理其他用户请求或执行其他任务。在所有任务都提交到线程池后,主线程可能会执行其他逻辑,最后调用 shutdown 方法来优雅地关闭线程池。


线程池执行流程图解

当你调用:

executorService.submit(() -> {
    // 任务逻辑
});

实际上背后会触发 ThreadPoolExecutor 的如下处理逻辑:线程池内部执行流程

  1. 核心线程数未满
    • 立即创建新线程来处理该任务。
  2. 核心线程数已满,进入下一步:
    • 将任务放入任务队列(如 LinkedBlockingQueue)。
  3. 任务队列已满,进入下一步:
    • 判断当前线程数是否小于最大线程数:
      • 如果小于,创建非核心线程来执行任务;
      • 如果等于最大线程数,触发 拒绝策略
  4. 执行完毕的线程会回到池中等待下一个任务(如果有),非核心线程在空闲超时后会被销毁

结合你的业务场景做解析

你这个订单场景中,共有 四个异步任务

步骤 任务名称 是否必须顺序 是否耗时
验证订单
计算金额
写入数据库 否(有依赖)
发送确认邮件

“不一定必须顺序”,是基于“线程池异步任务” 的并发角度而言的 —— 如果某些任务之间没有直接依赖关系,那就可以并发异步执行,提高效率。

但是你指出了一个关键点:这个场景中每个步骤其实是有依赖关系的,这种顺序性是业务逻辑所要求的,不是线程池可以随意打乱的

✅ 这正是线程池最适用的场景:任务独立 / 可并行处理 / 需要节省响应时间

🧠 所以在这个场景中,线程池的正确使用方式应该是:

我们不能把所有任务都直接丢进线程池 并发执行,而应该是:

主线程:
    1. 验证订单(同步执行)
    2. 计算金额(同步执行或异步计算后 get() 等待结果)
    3. 写入数据库(依赖前面两个成功)
    4. 发送确认邮件(可异步执行)

✅ 其中 步骤 ④ 发送邮件 是唯一适合异步执行的 —— 它不影响主流程,可以交给线程池异步去发。

订单处理是有严格先后顺序依赖的

  • 验证 → 金额 → 入库 → 邮件 是一条主流程链;
  • 线程池 不是用来打乱这个顺序的,而是用来 并发执行不影响主流程的附属任务(如邮件通知、日志、异步推送等);

使用线程池的好处:

  • 🚀 提升响应速度:主线程快速返回(如 UI/接口响应),耗时任务交给线程池异步完成。
  • 防止线程资源滥用:相比每个任务都new Thread(),线程池能重用线程,避免频繁创建/销毁造成资源浪费。
  • 🔒 线程复用 + 并发控制:通过设置 corePoolSizequeueCapacity 控制系统负载。
ExecutorService executorService = new ThreadPoolExecutor(
    5,                // 核心线程数
    10,               // 最大线程数
    60,               // 非核心线程的空闲存活时间
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100), // 队列长度
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

🎯 参数如何选择:

参数 选型建议
corePoolSize 可设为CPU核心数或稍高,如 Runtime.getRuntime().availableProcessors()
maximumPoolSize 视任务复杂度、响应时间等决定,通常为核心数的 2~3 倍 2N+1
queueCapacity 适合使用有限队列(如100),避免OOM
keepAliveTime 一般设置 30~60 秒
拒绝策略 建议使用 CallerRunsPolicy 或自定义策略
什么时候不该用线程池?

虽然线程池非常好,但在这些情况下应该慎用:

场景 建议方式
任务轻量、低频 可直接 new Thread
任务耗时严重、不确定性高 可用异步消息队列或限流机制替代
并发极高,超出系统承载能力 应加熔断、降级、防抖等保护机制
总结线程池应用场景
应用场景 是否推荐 理由说明
✅ 异步任务 推荐 提升响应速度,释放主线程
✅ 数据汇总 推荐 多接口并行调用、加速结果收敛
✅ 定时批处理任务 推荐 比如定时拉取、导入数据、分段分页并发处理等
❌ 高频IO长耗时任务 慎用 应考虑消息队列+异步消费,避免线程被长时间占用

你单独部署过项目吗?

前端打包ng配置文件
git所有人都用 需要拉分支 maven打包后端 包放到远程服务器 java -jar 启动!【不应该有人去做】
有专门工具去流水线制作 → Jenkins是一个开源的自动化服务器,它可以帮助您实现自动化构建、测试和部署项目 JenKins + docker 做自动化部署
部署是建立本地的项目再推到服务器

安装 Docker:确保你的部署服务器上安装了 Docker。
安装 Jenkins:可以通过 Docker 安装 Jenkins,或者直接在服务器上安装。

步骤 1:安装 Jenkins 和 Docker

确保 Jenkins 和 Docker 在服务器上都已经安装并运行。

步骤 2:配置 Jenkins

  1. 启动 Jenkins

    使用 Docker 启动 Jenkins:

    docker run -d --name jenkins -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts
    
  2. 访问 Jenkins:在浏览器中访问 http://<服务器地址>:8080,并按照指示完成 Jenkins 的初始设置。

  3. 安装必要的插件:安装 Docker、Git 等相关插件。

步骤 3:创建 Jenkins 任务

  1. 新建任务:在 Jenkins 主页上,点击“新建任务”。

  2. 配置源码管理:配置 Git 仓库地址。

  3. 配置构建触发器:选择合适的触发器。

  4. 配置构建环境:勾选“Build inside a Docker container”。

  5. 添加构建步骤

    • 执行 Shell

      docker build -t myapp .
      
  6. 添加构建后操作

    • Push built image:如果需要将镜像推送到 Docker 仓库,填写仓库信息。

    • 执行 Shell

      docker stop myapp || true
      docker rm myapp || true
      docker run -d --name myapp -p 8080:8080 myapp
      

步骤 4:执行构建

保存配置后,可以手动触发构建或者等待触发器自动执行构建。

步骤 5:验证部署

构建完成后,访问服务器的指定端口(例如 http://<服务器地址>:8080),验证应用是否成功部署。

你的期望薪资?

我目前的薪资是8000,考虑到我即将承担的职责和我的职业发展,我期望的薪资是在现有基础上有所提升,大约在8000到10000之间。当然,我对整体的薪酬包[包括福利、奖金和职业发展机会]也很感兴趣。薪资是如何构成的,包括固定工资、奖金、股权、福利等。

线程池场景题

核心线程数5个,最大线程数设置了10个,队列也设置了10个,现在有并发6个任务来,线程池中有多少个任务?

在您描述的线程池配置下,当有6个并发任务到来时,这些任务的处理情况如下:

  • 核心线程数是5,意味着线程池会首先创建5个线程来处理任务。
  • 当第6个任务到来时,由于核心线程都在忙,线程池会将这个任务放入队列中,因为队列的大小也是10。

所以,在这种情况下,线程池中会有6个任务:5个任务正在被5个核心线程处理,另外1个任务在队列中等待。线程池并没有达到最大线程数10个,因为当前的任务数量和队列容量还未超过核心线程数和队列的总和。

在您提供的线程池配置下(核心线程数5个,最大线程数10个,队列容量10个),当6个并发任务到来时,线程池不会立即创建10个线程,原因如下:

  1. 核心线程数优先:线程池首先会使用核心线程来处理任务。核心线程数是5,所以前5个任务会分别由5个核心线程来处理。
  2. 队列缓冲:当核心线程都在忙碌时,额外的任务会被放入队列中等待,而不是立即创建新的线程。您的队列容量是10,足以容纳当前的第6个任务。
  3. 按需创建线程:线程池会根据任务的处理速度和队列的饱和度来决定是否需要创建超出核心线程数的线程。在您的例子中,尽管有6个并发任务,但队列还未满,因此没有必要创建额外的线程。
  4. 最大线程数限制:最大线程数是线程池可以创建的线程数量的上限,但这并不意味着线程池会一开始就创建到这个上限。只有当队列满了,且还有新的任务到来时,线程池才会创建额外的线程(最多达到最大线程数)来处理这些任务。

因此,在您的场景中,当6个并发任务到来时,线程池的操作是:

  • 5个核心线程各自处理一个任务。
  • 第6个任务被放入队列中等待。

此时,线程池中只有5个线程在运行,队列中有1个任务,总共6个任务。线程池不会创建额外的线程,因为当前的任务数量还未超过核心线程数和队列的总容量。只有当队列满了(即有10个任务在队列中),且还有新的任务到来时,线程池才会考虑创建额外的线程,直到达到最大线程数10个。

get请求和post请求的区别

get请求
  • 请求指定的资源。使用GET的目的是获取数据,
  • 数据在URL中传输,通过将数据附在URL之后,以查询字符串的形式出现
  • 由于数据在URL中可见,因此安全性较低,敏感数据不应通过过GET请求发送
  • URL长度限制通常在2000个字符左右,这意味着GET请求能够传输的数据有限
  • 可以被缓存,也会被浏览器保存在历史记录中
  • 常用于信息查询、数据检索等操作.
post请求
  • 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。
  • 数据存储在请求体(HTTP消息主体)中,不会密在URL上
  • 数据不会出现在URL中,相对更安全,适合传输敏感信息。
  • 理论上没有大小限制,适用于传输大量数据.
  • 不会被缓存,且不会保存在浏觉器历史记录中
  • 常用于数据提交、表单提交等操作

请求行:请求类型 请求方法 url http版本1.1 老式1.0不支持长连接
请求头:key value
常见请求头: 请求数据类型,restful基于json
Content-Type:上传文件不用application 要用 multipart/form-data”
Host:指定请求的服务器的域名和端口号。
User-Agent:包含发出请求的用户代理软件信息,通常包括浏览器类型和版本
请求体:get请求可以有请求体
响应:响应行 响应体 状态码 描述
常见响应头Content-Type:返回数据的格式 Content-Length:响应体的长度,以字节为单位

post请求也可以用问号的形式拼接到浏览器 也可以用路径参数

很大区别:get一般放在url后面 会展示url和后面参数 会暴露传参隐私 登录接口用post来做 有密码敏感信息
表单、密码、长数据用post 不过怕黑客抓包 相对来说安全
get请求后面传参的大小限制 理论上没有限制 只是浏览器厂商会有限制
get用来查询 post新增提交表单

“GET 和 POST 都是 HTTP 协议中最常用的请求方式,最本质的区别在于数据传输的位置与幂等性,不仅影响安全性和缓存行为,也关系到实际应用场景选择。”

✅ GET 与 POST 请求的对比总结表

项目 GET 请求 POST 请求
作用 查询资源(获取数据) 提交资源(发送数据)
数据位置 URL 参数:拼接在 ? 后面,如 /user?id=1 请求体:放在 HTTP 请求体中
是否显示参数 ✅ 显示(暴露在 URL 中,用户可见) ❌ 不显示(隐藏在 Body 中)
是否安全 ❌ 不安全(敏感信息容易泄露,如明文密码) ✅ 相对安全(但也要注意防止抓包等问题)
参数长度限制 有限制(由浏览器和服务器限制,一般不超过 2KB) 理论上无限制(受服务器配置影响)
是否可缓存 ✅ 可以被缓存 ❌ 默认不可缓存
是否保留历史记录 ✅ 会被浏览器记录 ❌ 不会记录
是否能被收藏为书签 ✅ 可以(URL完整) ❌ 不建议(没有完整 URL 参数)
应用场景 查询、检索、列表展示 提交表单、上传文件、登录、注册
请求幂等性 ✅ 幂等(多次请求结果一样) ❌ 不一定幂等(多次提交可能创建多条数据)
常用请求头 AcceptUser-AgentHostAuthorization 额外使用:Content-Type(如:application/jsonmultipart/form-data
是否可附带请求体 ✅ 理论支持,但不推荐使用,很多服务端框架默认忽略(如 Spring MVC) ✅ 正常使用请求体(Body)传参
是否支持路径参数 ✅ 支持,如 /user/1 ✅ 同样支持路径参数和 query 参数(但参数多用 Body 传)
是否可以拼接 ? 参数 ✅ 常用,如 /api?key=123 ✅ 也可以 /api?token=abc,但数据主体依然建议放在请求体中
对缓存友好程度 ✅ 友好,可被浏览器缓存,也可用于 CDN 缓存 ❌ 默认不可缓存(除非通过响应头指定)
在 RESTful 中常用场景 获取资源,如 GET /api/users 创建资源,如 POST /api/users,或提交登录等敏感操作

冷门但常考 & 实战必备考点

冷门知识点 / 实战经验 说明
GET 请求也可以有请求体 HTTP 协议允许,但很多框架(如 Spring)不解析,默认忽略
POST 请求也可以拼接 URL 参数 ✅ URL 参数与请求体可同时存在,如:POST /api?type=1 + body 传参
浏览器最大 URL 长度限制 ❗ 通常为 2KB~8KB,IE 最小Chrome 支持更长
POST 也可以被缓存 ❌ 默认不会缓存,但可通过 Cache-Control 或自定义缓存策略强制缓存
GET 请求一般不能上传文件(除非用 Base64 等特殊形式) 文件上传推荐用 POST + multipart/form-data
登录操作必须用 POST 因为 GET 暴露用户名/密码容易被记录,敏感数据必须走 POST + HTTPS 加密传输
GET 请求参数对顺序敏感 某些框架(如部分网关、签名认证)会对 URL 参数顺序敏感,应注意参数顺序一致
Content-Type 区别 - GET 请求常省略或默认 - POST 常设定为: ① application/json ② application/x-www-form-urlencoded ③ multipart/form-data
GET 请求会被预检(preflight)吗? ❌ 不会,只有跨域的非简单请求(如 PUT、带自定义头的 POST)才会有 OPTIONS 预检

是否上传过图片

阿里云是最后存储的
完整的上传图片应该:
前端要配合(表单 post提交) Content-Type:上传文件不用application 要用 multipart/form-data” 同一个请求能边上传图片和文本数据
后端的操作:传到后端controller接收,有一个类multipart 专门接收二进制数据 图片视频等,有很多api → get input stream封装util 调用upload上传。中小型企业都用阿里云oss 因为要考虑容灾 地震 容易数据丢失,要考虑备份→集群,服务器有物理硬件上限(要有运维成本),文件维护很专业交给专业的人。阿里云的机房在深圳,广州的人访问会比哈尔滨的更快。光纤受物理限制 越长越有损耗。大型运营商在全国各地都有机房,可以智能判断比如哈尔滨的就去访问黑龙江服务器。CSDN内容分发(收费)

🔧 完整的上传流程

环节 说明
前端 使用 form 表单或 Ajax 上传,Content-Type 设置为 multipart/form-data,支持同时传文本+文件
控制器 Spring Boot 使用 MultipartFile 类型接收上传文件,可使用 .getInputStream() 获取内容流
上传工具类 将文件流封装上传到第三方云存储,如阿里云 OSS、七牛云、腾讯云等
云存储 云厂商拥有备份、容灾、集群、分发等能力,确保数据安全和访问速度
返回前端 返回上传后的图片 URL 或文件 ID 等信息

前端传过来的图片怎么设置图片大小 有没有什么办法?

思考:为什么后端要限制前端图片传的大小?
大图片 + 多人上传,首先后端要读到jvm内存再二进制流给到阿里云,同时并发有可能超出OM的java内存大小
springboot yml加文件上传大小配置

spring:
  servlet:
    multipart:
      max-file-size: 10MB # 单个文件的最大大小
      max-request-size: 20MB # 整个请求的最大大小,包括多个文件的总和
  1. 读取到 JVM 内存
    • 当前端发送图片文件到后端时,后端服务器需要接收这个文件的数据。
    • 在 Java 应用程序中,接收到的文件数据首先会被加载到 JVM(Java 虚拟机)的内存中。这是因为在 Java 应用程序中处理任何数据之前,数据必须先被加载到内存中。
  2. 二进制流给到阿里云
    • 一旦文件数据被加载到 JVM 内存中,后端服务通常会将这些数据以二进制流的形式上传到云存储服务,比如阿里云的对象存储服务(OSS)。
    • 这个过程涉及到数据的读取和写入操作,即从 JVM 内存读取数据,然后写入到云存储服务。
  3. 并发可能导致内存溢出
    • 如果有多个用户同时上传大图片,后端服务器可能会同时处理多个上传请求。
    • 每个上传请求都会占用一部分 JVM 内存。如果上传的图片非常大,且并发请求的数量很多,那么所有请求加起来的内存使用量可能会迅速增加。
    • 如果内存使用量超过了 JVM 分配给应用程序的内存大小(即 OutOfMemory,简称 OOM),就会发生内存溢出错误。这种错误会导致应用程序崩溃或者变得不稳定。
  4. 限制上传大小的重要性
    • 为了防止内存溢出错误,后端通常会限制上传文件的大小。
    • 通过限制单个文件的最大大小(max-file-size)和整个请求的最大大小(max-request-size),可以有效地控制内存的使用,避免因大量并发上传大文件而耗尽服务器内存。

前端图片大小如何限制?

✅ 原因:避免后端 OOM(内存溢出)

步骤 原因
上传前 浏览器本地可限制图片大小或压缩(前端 UI 限制)
上传中 设置 multipart/form-data 协议
后端接收 文件先被加载到 JVM 内存,再传给云服务,大图 + 并发上传 = 高内存占用
限制大小 避免 OOM,保障服务稳定性

✅ Spring Boot 限制配置

spring:
  servlet:
    multipart:
      max-file-size: 10MB         # 单个文件最大10MB
      max-request-size: 20MB      # 整体请求最大20MB(多个文件+字段)

控制器代码示例(Java)

@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
    if (file.isEmpty()) {
        return "上传失败,文件为空";
    }

    try (InputStream inputStream = file.getInputStream()) {
        // 上传到阿里云OSS的逻辑
        String url = ossUtil.upload(inputStream, file.getOriginalFilename());
        return "上传成功,访问地址:" + url;
    } catch (IOException e) {
        e.printStackTrace();
        return "上传失败:" + e.getMessage();
    }
}

冷门但常考的扩展点

知识点 说明
MultipartFile 是什么? 是 Spring 封装的文件上传类型,内部包含文件名、文件大小、文件流等
同一请求能传图片+文本? ✅ 支持,表单中可同时上传 file + 文本字段,如商品名、描述
阿里云的上传路径建议? 可使用时间戳+UUID组合路径,如:images/2025/07/abc.jpg,防止重复文件覆盖
为什么选择 OSS? 专业、安全、弹性扩展、支持 CDN 分发、支持跨区域访问
OSS 上传是否走 JVM? 是的,文件先读入 JVM,再通过 SDK 上传,但也可以优化为直传(前端直传 OSS)减少后端压力
会不会并发导致 OOM? 是的,尤其是大文件 + 并发上传,若不限制上传大小,极易导致内存溢出

你在里面主要负责哪方面的工作?

我之前负责后端开发 也会参与一部分设计工作
开发完会协助测试 和前端进行联调
和组长一起进行测试
和前后端的逻辑基本上都是可以的

上家公司的离职原因,薪资多少,薪资结构

不要说一些面试官能挑刺的理由
发展前景?表明上家公司不好
太想进步?表名上家公司提供的技术不好 自己技术不好
在上家公司我学习了很多 成长了很多,个人发展原因 ,想要涨薪

// 来自AI的答案 仅供参考
我在上家公司学到了很多,但我觉得为了我的职业发展,我需要寻找一个能够提供更多成长机会和挑战的职位。我想要在[技能/领域]上进一步深耕,而贵公司的职位看起来非常符合我的职业规划;我在上家公司的年薪大约在6000到7000之间;我的薪资结构主要包括基本工资、每年两次的绩效奖金、股票期权以及一些标准福利,比如健康保险、退休金计划等。此外,公司还提供了一些额外的福利,比如灵活的工作时间和远程工作的机会
简历公司

上家公司如果问工作不好找 为什么不先找到再离职
我在这一块想好好准备面试 但是上班的时间不好分配 我想专心去找工作
上家工作繁忙抽不出时间去准备 所以我想多多准备
异地公司 → 万能理由:现在面的公司在哪家里人就在哪[地理位置要接近 精确到哪个城市] 异地很多都线下不方便先离职专心准备

薪资多少

现在期望12k 上家最好保证**20%-30%**区间→8-9-10k(参考城市不同)

薪资结构

基本工资(七八成)+绩效工资(20%-30%) 有公司先扣除 有的当月发
A 120% S 150%-200% C 80%

你对上家公司的看法

不能贬低 要说优点 学习成长了很多 同事和领导都很照顾我

什么时候能入职?

三个工作日 到 一周之间

你离职了 现在有多少个offer了?

不能说一个都没有

  • 我已经有2个offer 但是一定要表达对当前公司的期待 经过我的了解 我更喜欢贵公司的发展和文化
  • 我也是刚刚开始找工作…

你可以接受加班吗

(必须完全接受全部加班 先拿到offer再说)

Controller和RestController的区别

@RestController = @Controller + @ResponseBody

@Controller如果要返回JSON/XML等格式的数据给客户端,必须显式的使用@ResponseBody注解将返回的对象转换为HTTP响应体内容。
@RestController 专门为构建RESTful Web服务设计的控制器。它简化了创建API的过程,因为所有方法默认都会将返回值直接写入HTTP响应体中作为JSON或XML格式的数据。

@Controller可以声明一个类为一个bean 控制器用
@ResponseBody 具体方法和类都可以 不是包装类和字符 都可以自动转成json数据格式 更符合restful风格

@RestController 是 Spring 为了简化 RESTful 接口开发而推出的复合注解,等价于 @Controller + @ResponseBody。它让我们开发 API 接口时无需再为每个方法添加 @ResponseBody,所有返回值会自动转换为 JSON 或 XML 格式写入响应体,非常适合前后端分离项目。”

在yaml文件中定义了一些参数,该怎么调用

  • 使用 @Value 注解,这是最直接的方式,适用于简单的属性注入。是bean的注解 用${key}还可以用#
    • ${}:用于注入外部配置文件的值。它告诉Spring需要从环境变量、属性文件、系统属性等地方查找相应的值。
    • #{}:用于执行SpEL(Spring Expression Language,Spring表达式语言)表达式。它允许你在注入值时执行一些简单的计算或逻辑。
    • 如果在多个类里引用 配置多 杂乱 可以写个配置类写一堆的属性 提供get set方法 配置类.get获取到配置
# application.yml
server:
port: 8080

custom:
property: myCustomValue
number: 42
enabled: true
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class CustomComponent {

 // 注入server.port属性
 @Value("${server.port}")
 private String serverPort;

 // 注入custom.property属性
 @Value("${custom.property}")
 private String customProperty;

 // 注入custom.number属性,并转换为int类型
 @Value("${custom.number}")
 private int customNumber;

 // 注入custom.enabled属性,并转换为boolean类型
 @Value("${custom.enabled}")
 private boolean customEnabled;

 // 使用SpEL表达式来计算值
 @Value("#{${custom.number} * 2}")
 private int doubleCustomNumber;

 // 使用SpEL表达式来获取环境变量
 @Value("#{systemProperties['os.name']}")
 private String osName;

 // ... 使用注入的值进行操作

 // Getter和Setter方法
 // ...
}
  • 使用 @ConfigurationProperties 注解,通常会指定一个前缀prefix),这个前缀用于指定配置文件中哪些属性应该被绑定到这个 Bean 上。然后确保主应用程序类或某个配置类上有@EnableConfigurationProperties(AppProperties.class) 注解
    @ConfigurationProperties(prefix = "prefix")

IOC和DI有了解过吗,它们的好处是什么

它们的目的是为了解耦
IOC(控制反转)是Spring的两大核心之一,DI(依赖注入)
IOC把控制权交给spring容器
对象创建好之后 之间会有依赖关系 DI因此而生
实现方式:DI通常有四种实现方式

IOC 与 DI 总结对比表

概念 IOC(控制反转) DI(依赖注入)
含义 控制权从代码“主动”创建对象,反转给 Spring 容器 Spring 容器将依赖对象“注入”到目标对象中
目标 解耦合,管理对象生命周期和依赖关系 实现 IOC 的核心手段,降低模块耦合度
体现 Bean 由容器统一创建和维护 对象的依赖由容器自动提供
好处 降低耦合、统一管理对象 提高可维护性、扩展性、便于测试
关系 是一种设计思想 是实现方式(IOC 的具体实现手段)

IOC & DI 经典解释

  • IOC(控制反转) 是一种思想,把原来由开发者手动 new 对象的工作交给 Spring 容器来做,实现对象创建和依赖管理的统一控制
  • DI(依赖注入) 是实现 IOC 的一种方式,即通过注解、构造函数等方式,由容器注入依赖对象

📌 举个栗子:

类似租房平台,房东提供房子(Bean),中介平台(Spring 容器)统一管理,租客(业务类)不直接找房东,而是由平台分配依赖。

  • 属性注入 注解注入

    • @Autowired 是 Spring 提供的注解,用于自动装配 Bean。它可以用于字段、构造函数、方法或设置器上。当 Spring 容器启动时,它会自动查找并注入匹配的 Bean。
      • 偶尔有不影响程序运行的报错?写spring技术人员是根据jdk写,怕别人不用。
    • @Resource 是 Java 的注解[JDK的],用于依赖注入,它也可以用于字段、方法或设置器上。与 @Autowired 不同的是,@Resource 默认通过名称进行匹配,如果未指定名称,则尝试通过类型进行匹配。
    • 两者区别
      • @Autowired 先根据属性类型 去容器里面找 如果找不到 再根据**属性名称[字段]**去找 如果实在找不到就会报错 [@Autowired永远不会放弃你的 尽其所能去帮你找]
      • @Resource 先根据属性名称去找 要么找不到 要么找到一个 找到就去注入 如果找不到 可以再根据属性类型去找 [类型找不到 或者 找到多个 也会报错]
    它俩最大的区别是什么?

    @Autowired 更倾向于按类型注入,如果类型不唯一,则需要指定注入的名称。
    @Resource 更倾向于按名称注入,如果没有指定名称,则尝试按类型注入。
    两者的不同在于默认的注入策略和如何处理不唯一的 Bean 定义。

    @Autowired@Resource 最大的区别在于它们的默认注入策略和所依赖的注入机制:

    1. 默认注入策略
    • @Autowired:默认是按照类型(Type)进行注入的。如果容器中存在多个相同类型的 Bean,则需要通过 @Qualifier 注解指定具体的 Bean 名称,或者通过设置 @Autowiredrequired 属性为 false 来允许没有找到匹配的 Bean 时不抛出异常。
    • @Resource:默认是按照名称(Name)进行注入的。如果未指定名称,则尝试按类型进行注入。如果容器中存在多个相同类型的 Bean,且没有指定名称,可能会抛出异常。
    1. 依赖的注入机制
    • @Autowired:是 Spring 框架提供的注解,因此它只能用于 Spring 管理的上下文中。
    • @Resource:是 Java 的扩展包(javax.annotation)提供的注解,它是 JSR-250 规范的一部分,因此可以在任何实现了 JSR-250 规范的容器中使用,不仅限于 Spring。

    简而言之,最大的区别在于 @Autowired 更侧重于类型匹配,而 @Resource 更侧重于名称匹配,并且 @Resource 是 Java 标准的一部分,具有更广泛的适用性。

    @Autowired vs @Resource 区别总结表

    特性 @Autowired @Resource
    提供方 Spring JDK(JSR-250)
    默认注入方式 按类型(byType) 按名称(byName)
    是否支持按类型注入 ✅ 支持 ✅ 支持(次选)
    是否支持按名称注入 ✅ 需搭配 @Qualifier ✅ 默认
    是否推荐 ✅ 推荐 ⚠️ 部分场景适用
    可用位置 构造方法、字段、方法 字段、方法
    是否支持多个构造方法 ❌ 不能多个都加 @Autowired ❌ 同理

    ✅ 开发建议

    场景 建议
    只有一个依赖对象 优先使用构造器注入,语义清晰
    需要注入多个依赖 使用字段注入或构造器注入
    对依赖可选性较强 可考虑 setter 或普通方法注入
    想确保更强约束性 使用构造器注入(+final属性)

    “IOC 是一种反转控制权的思想,由 Spring 容器来统一管理对象的创建和依赖;而 DI 是实现 IOC 的手段,通过构造器、属性等方式将依赖对象注入到类中。它们的最终目的是为了解耦,提高代码的可维护性和扩展性。开发中常用 @Autowired@Resource 实现属性注入,构造器注入则更具可读性和测试友好性。”

  • 构造函数注入 [默认生成空参构造方法 若写有参构造原来无参会被覆盖 参数根据类型去找和@Autowired类型一样 可以写多个构造方法 如果去多个构造方法重载会报错 怎么办?加个@Autowired[属性,构造方法,参数]都可加 不可多个方法都加@Autowired 反射会触发构造方法 @Bean => new ]

    public class MyService {
    
        private DependencyA dependencyA;
        private DependencyB dependencyB;
    
        // 构造函数注入
        @Autowired
        public MyService(DependencyA dependencyA) {
            this.dependencyA = dependencyA;
        }
    
        // 另一个构造函数
        @Autowired
        public MyService(DependencyB dependencyB) {
            this.dependencyB = dependencyB;
        }
    
        // ... 其他方法 ...
    }
    /////////////////////////////////////////////////////
    在上面的例子中,由于有两个构造函数都使用了 @Autowired 注解,Spring 将无法确定使用哪一个构造函数,因此会抛出异常。要解决这个问题,你应该只在一个构造函数上使用 @Autowired 注解。
    
  • Set方法注入[原生spring 用xml去定义才有 SpringBoot没有这个注入 ]

    public class MyService {
    
        private DependencyA dependencyA;
    
        // Set 方法注入
        @Autowired
        public void setDependencyA(DependencyA dependencyA) {
            this.dependencyA = dependencyA;
        }
    
        // ... 其他方法 ...
    }
    //////////////////////////////////////////////////////
    在 Spring Boot 中,虽然不常用 XML 配置,但是你仍然可以通过注解来实现 Set 方法注入。
    
  • 普通方法注入

    public class MyService {
    
        private DependencyA dependencyA;
    
        // 普通方法注入
        @Autowired
        public void init(DependencyA dependencyA) {
            this.dependencyA = dependencyA;
        }
    
        // ... 其他方法 ...
    }
    //////////////////////////////////////////////////////
    普通方法注入指的是在类中的任意非构造函数方法上使用 @Autowired 注解
    

测试过程有没有出现反复的困扰?

客户需求频繁更改
测试用例没有覆盖到
开发和测试环境未协调

太复杂的改动要先报备技术经理、项目经理

测试:自测 单元测试 专业人员

公司使用哪些技术?

后端:Redis RabbitMQ 搜索引擎 微服务常用组件 远程调用 统一网关 Springboot Springcloud MybatisPlus

项目有多少个成员?

2前 8后 1测 1运维 1项目经理(小公司约13人左右) 要具体人数
自研公司?外包?

自研公司

  • 创业型自研公司:通常员工人数在10-50人之间,初期可能更少,只有几人到十几人。
  • 成熟自研公司:员工人数可能从几十人到几百人甚至更多。

外包公司

  • 小型外包公司:员工人数可能在10-50人之间。
  • 中型外包公司:员工人数可能在50-200人之间。
  • 大型外包公司:员工人数可能超过200人。

HashMap底层原理

底层数据结构

jdk1.8之前底层结构是数组+链表(key+value) 数据结构通用的[键值对+哈希表的数据结构]
jdk1.8以后【数组+链表+红黑树】在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时且数组长度大于64时,将链表转化为红黑树,以减少搜索时间。扩容时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。后期使用map获取值时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
底层的地址运算出来 如果地址不一样计算出来的hashcode不一样,hashcode一般是数字[整数(±或0)] 通过key进行hashcode运算 对数组长度取模 eg:任何整数去取模10 可以定位到value可以放在哪个桶下面

hashcode本身不同的对象算出来的hashcode值是相同的怎么办呢?
两个value不可能放在同一个桶 这就是hash冲突 如果数组长度是8 算出来一个hashcode值是8 和 16 此时取模余数相同这样的情况也是相同。所以此时应该用拉链法[小葡萄串],即使桶一样 可以用指针一个个指,此时的链表是单向链表[linkedlist才是双向链表]。
在1.8后指出如果同一个桶的葡萄串太多了,此时要拿出数据,时间复杂度就是O(n),如果没有很多就是O(1)数组的长度。红黑树的引入是解决链表过长的问题。
红黑树是树形的高级数据结构 时间复杂度O(logn)
二叉树在某些情况下会退化成链表 右子树永远比根节点大
红黑树会旋转自平衡[局部旋转达到平衡] 超过多少层会旋转 不至于退化成链表。

拉链法(Chaining)是 Java 的 HashMap 在 JDK 1.8 之前以及之后都使用的一种解决哈希冲突的方法。在 JDK 1.8 之前,HashMap 的实现主要是基于拉链法,即使用链表来解决哈希冲突。当不同的键通过哈希函数计算出相同的哈希码,并且映射到同一个桶(bucket)时,这些键值对将以链表的形式存储在同一个桶中。

在 JDK 1.8 中,HashMap 的实现进行了改进,当链表的长度超过一定阈值(默认是 8)时,链表会被转换成红黑树。这是为了优化哈希表的性能,特别是当哈希冲突严重时,链表的查询效率会降低到 O(n),而红黑树可以将查询效率提升到 O(log n)。

红黑树如何避免哈希冲突

红黑树本身并不直接解决哈希冲突,而是优化了哈希冲突发生后的数据结构。以下是红黑树在 HashMap 中是如何工作的:

  1. 哈希冲突:当不同的键产生相同的哈希码或经过取模运算后落在同一个桶时,就会发生哈希冲突。
  2. 链表:在 JDK 1.8 中,如果桶中的元素少于一定数量(默认为 8),就会使用链表来存储这些元素。
  3. 红黑树转换:当链表的长度超过阈值(默认为 8)时,并且数组的长度超过 64,链表会被转换成红黑树。这样可以减少查找时间,因为红黑树是一种自平衡的二叉搜索树。
  4. 自平衡:红黑树通过旋转和重新着色操作来保持树的平衡,从而避免了二叉搜索树退化成链表的情况。

红黑树的旋转和自平衡

红黑树通过以下规则保持平衡:

  • 节点颜色:每个节点要么是红色,要么是黑色。
  • 根节点:根节点是黑色的。
  • 红色规则:如果一个节点是红色的,则它的子节点必须是黑色的(不能有两个连续的红色节点)。
  • 黑色高度:从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

当插入或删除节点时,可能会破坏这些规则,此时红黑树会通过以下操作来重新平衡:

  • 左旋转:当右子节点是红色,而左子节点是黑色或不存在时,进行左旋转。
  • 右旋转:当左子节点是红色,并且它的左子节点也是红色时,进行右旋转。
  • 重新着色:在某些情况下,通过改变节点颜色来维持红黑树的性质。

通过这些操作,红黑树确保了即使在最坏的情况下,树的高度也不会超过 2log(n+1),从而保证了 O(log n) 的时间复杂度。

总结来说,红黑树并不直接解决哈希冲突,而是优化了哈希冲突后的数据结构,使得即使在发生大量哈希冲突的情况下,HashMap 的性能也不会显著下降。

扩容机制

new的初始化 数组为空
当第一次put的时候才不会为空 为16
扩容因子为什么是0.75?hashmap不仅仅java 其他语言也有这种数据结构 但扩容因子可能不同,是通过大量的数学概率统计出的最好最平衡的值。达到12的时候会扩容成2倍
new一个hashmap可以指定数组长度为7 此时数组长度是8【hashmap的长度永远是2的幂次方 比你传入的长度永远大 且 是2的幂次方】 为什么2的幂次方?因为1.7要数组取模 怎么打这个符号 shift+5 => %,1.8之后用了位运算,>>2 <<2 让你的取模运算更快。如果出现hash冲突会拉链 当它的数组长度大于64 并且 链表长度大于8时,当链表长度小于等于6临界值会变回来【为什么是6?避免频繁切换(离8太近) 链表 ←→ 红黑树[消耗性能]】

链表1.7之前是头插法 会产生一条首尾相接的死循环【并发情况[但是hashmap线程不安全不会用在并发,要用ConcurrentHashMap]一起put 且 同一个桶】
1.8之后是尾插法,并发情况下不会出现cpu飙高

HashSet底层数据结构

底层是包装了一个hashmap,无序 key不允许重复 value可重复
HashSet单列无序不重复的 key就是那个元素 value就是new了一个无意义的object对象

ArrayList和LinkedList不是线程安全的 用什么?

  • Vector 读写都加锁。
  • CopyOnWriteArrayList 读不加锁 写加锁

ConcurrentHashMap能存null吗?不允许使用 null 作为键,但是允许使用 null 作为值。
HashMap:null默认放在第一个桶下面 下标写死为0

Hashcode相同equals一定相同吗?

hashCode() 相同不一定意味着 equals() 相同,但 equals() 相同则 hashCode() 必须相同。

  • equals() 方法用于判断两个对象是否逻辑上相等。
  • hashCode() 方法用于返回对象的哈希码,这个哈希码通常用于哈希表的快速查找。

key可以放复合对象,要注意要重写 hashcode()和equals() 如果不重写 new了的两个对象有可能会相同
"重地""通话"计算hashcode会比equals更快,一个对象new出来后hashcode已经计算出来了。equals要比较每个对象值,所以先判断hashcode 再判断equals 重写:@Override 用属性里面的hashcode,user里面包含了复杂对象order 此时order也要重写。包装类已经重写了hashcode,要整个对象返回true才为正确的,要层层递进去判断。hashmap重写复杂对象就一定要重写那俩个 ∵ 是比较对象里的属性值
list 有序可重复单列
map 双列key不能重复value可重复 treemap是有序的
set 单列不重复无序 hashset 无序 treeset 有序

== 值 + 地址值
equals 是对象属性值是否一 一相等

hashCode()equals() 总结对比表

比较项 hashCode() equals()
作用 返回对象的哈希值 判断两个对象是否“逻辑相等”
定义位置 Object 类中定义,可被重写 同上
默认行为 返回对象内存地址的整数表示 比较是否为同一对象(地址)
需要重写的场景 自定义对象作为集合的 key(如 HashMapHashSet 自定义对象需要逻辑比较
是否与对象属性有关 默认无关,重写后应有关 重写时应只关心关键属性
性能 高效(整型值) 慢(逐个属性比较)

二者关系说明(核心规则)

判断 是否成立
equals() 返回 truehashCode() 必须相等 ✅ 必须成立
hashCode() 相等 → equals() 不一定相等 ⚠️ 可能不成立(哈希冲突

👉 这就是为什么 HashMap 在比较 key 时,先比 hashCode(),再比 equals(),为了提升性能,也确保正确性

String s1 = "重地";
String s2 = "通话";

System.out.println(s1.hashCode()); // 输出1179395
System.out.println(s2.hashCode()); // 输出1179395
System.out.println(s1.equals(s2)); // false

⚠️ 两个字符串的 hashCode() 相同,但 equals() 不相同,说明它们逻辑上不相等(只是哈希冲突)。

✅ 为什么要重写 hashCode()equals()

如果你将自定义对象用作集合中的 key(如放入 HashMapHashSet),那么:

  • 不重写:默认使用地址判断,相同内容的新对象视为不同
  • 正确重写:按属性判断内容一致性,可识别“逻辑相同”的对象
public class User {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User other = (User) o;
        return this.age == other.age && Objects.equals(this.name, other.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

equals() 用于判断两个对象是否逻辑相等,hashCode() 用于加快集合查找效率。在实际开发中,若你要将对象作为 HashMapHashSet 的 key,必须同时重写这两个方法,保证逻辑一致性,避免出现“相等的对象进不了集合”的问题。

HashMap是线程安全的吗

不安全的,可以使用ConcurrentHashMap线程安全、Collections.synchronizedMap()、HashTable
线程安全:多线程对同一个数据进行增删改是否受到影响
怎么办?

  • 加锁

    • synchronized
    • ReentrantLock

    加锁为什么能解决线程安全问题?线程访问资源的先后顺序
    多线程访问同一个数据 => 多个线程访问同一个数据
    秒杀 =>[思想] 1w个人买 对 100个库存进行扣减,只搞100个线程 把100个库存分成10份 其中每份有10个

    初始化库存: 创建一个共享的库存计数器,初始值为100。
    创建线程: 创建100个线程,每个线程在启动时分配到一个特定的库存分片。
    扣减库存: 每个线程尝试扣减其分配到的库存分片中的一个商品。扣减操作必须是原子的,以确保线程安全。
    同步机制: 使用适当的同步机制(如synchronized关键字、ReentrantLock等)来保护库存扣减操作,防止并发问题。
    库存检查: 在扣减前,线程需要检查当前分片是否有剩余库存。如果没有,则线程可以终止或进行其他处理。
    

    HashTable不管读写都会用synchronized加锁,并发一起来读都加锁 没必要,所以用了ConcurrentHashMap读不加锁 写加锁
    随着时间的推移,Hashtable 已经被认为是遗留代码,现代Java代码更倾向于使用 HashMap(非线程安全)或 ConcurrentHashMap(线程安全)。

Synchronized

public class SynchronizedExample {
    public synchronized void synchronizedMethod() {
        // 这里是同步代码块
        System.out.println("进入同步方法");
        // 执行一些操作
        System.out.println("退出同步方法");
    }
}
-----------------------------------------------------
public class SynchronizedBlockExample {
    private final Object lock = new Object();

    public void synchronizedBlock() {
        synchronized (lock) {
            // 这里是同步代码块
            System.out.println("进入同步代码块");
            // 执行一些操作
            System.out.println("退出同步代码块");
        }
    }
}

ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void lockedMethod() {
        lock.lock(); // 加锁
        try {
            // 这里是同步代码块
            System.out.println("进入锁定的方法");
            // 执行一些操作
            System.out.println("退出锁定的方法");
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

红黑树查询效率高的原因

红黑树是一种自平衡的二叉搜索树,它通过特定的规则来保持树的平衡,这些规则确保了树的高度大约是log(n)
自平衡 局部旋转

hashMap时间复杂度

  • O(1):不涉及到拉链
  • O(n):拉链不是树
  • O(logn):桶为红黑树

这个测试是你做的吗,还是你们团队去做的?

像自测的话是我自己去做的(测试用例、apifox),然后交给专业测试人员

团队是怎么协作的?

像我们团队的话,有使用禅道来做我们的文档管理,需求管理和需求的变更控制,和工作的一个统计报表,大部分的协作任务都可以在禅道上完成,代码这一块我们是使用git来做一个代码管理和协作的。

linux查看每个文件夹下的文件大小的命令

ls -lh

linux查看文件夹大小的命令

du -sh
du -sh –all 隐藏
du -sh ./* : 单独列出各子项占用的容量

linux查看进程的命令,动态查看一个文件的最后100行的命令

ps -ef
ps aux [查看所有用户的进程(包括其他用户的进程):]
ps -ef | grep mysql

动态:tail -n 100 -f xxx 【-f → follow】
静态:tail -n 100 xxx

常用的Linux命令

rm = romove

用于删除文件和目录
rm [-rf] name
-r(recursive递归):将目录及目录中所有文件(目录)逐一删除,即递归删除
-f(force):无需确认,直接删除

rmdir = remove directory

它用于删除空目录。如果目录不为空,即目录中包含文件或其他子目录,rmdir 命令将无法删除该目录

pwd = print working directory

打印出当前工作目录的绝对路径。当你需要知道你在文件系统中的当前位置时,这个命令非常有用

cp = copy

复制文件和目录。这个命令可以用来创建文件的副本或将文件从一个位置移动到另一个位置。
cp [-r] source dest
-r (recursive递归):如果复制的是目录需要使用此选项,此时将复制该目录下所有的子目录和文件

mv = move

为文件或目录改名、或将文件或目录移动到其他位置【移动 重命名 修改】

grep

用于搜索文本数据,特别是使用正则表达式来匹配指定的模式
查看特定进程的详细信息,例如进程名为 mysql
ps -ef | grep mysql

tar [tape archive]

用于打包多个文件和目录到一个归档文件中,或者从归档文件中提取文件

cd

切换路径

vim

编辑文件

cat

查看文件[head]

如何查日志

查看/var/log/user.log文件,并且想要跟踪用户 name:pcy 的活动

tail -f /var/log/user.log | grep “pcy”
高级专用使用awk 可以用正则等一些逻辑操作去获取日志

内建函数

awk 非常强大,可以用于执行复杂的文本分析和报告生成,awk 有许多内建函数,如 length()toupper()tolower() 等。

awk '{print toupper($0)}' filename  # 将所有内容转换为大写

条件语句

awk '{if ($1 > 100) print$1}' filename

循环

awk '{for (i=1; i<=NF; i++) print $i}' filename

数组

awk '{count[$1]++} END {for (word in count) print word, count[word]}' filename

你们接口是如何让前端调用的

我们会在设计阶段提前设计好给前端 并行开发 前后端联调[本地ip端口告诉前端]

接口文档怎么定下来的

根据页面原型、需求设计接口文档[后端自己写],绝大部分后端看原型的出参入参 无太大需求和前端商量。【前端组件库】[若修改返回结构的时候] [按照数据结构修改] 需要听前端意见

前端调用后端用的是什么请求方式

WebSocket【基于长连接通讯】
HTTP

前端开发中,以下是一些常见的使用场景:

  • 获取数据:使用GET请求。
  • 提交表单或数据:使用POST请求。
  • 更新资源:使用PUT或PATCH请求。
  • 删除资源:使用DELETE请求。

前端可以通过多种方式发起这些请求,例如:

  • 使用HTML表单(通常用于GET和POST请求)。
  • 使用JavaScript的XMLHttpRequest对象或者更现代的fetch API来发起各种类型的HTTP请求。
  • 使用各种前端框架和库(如React, Angular, Vue.js)中提供的封装好的HTTP服务。

SpringBoot主要的一些注解?都有哪些,以及主要作用

SpringBoot:
@SpringBootApplication [见↓↓]
@ConfigurationProperties:注解用于将外部配置(如来自properties文件、YAML文件或环境变量)绑定到JavaBean上。它的作用是将配置文件中的属性映射到JavaBean的属性上,这样就可以在应用程序中使用这些配置属性。
@SpringBootTest:用于测试 Spring Boot 应用,提供测试环境的支持
@EnableConfigurationProperties:启用对配置属性的支持,允许将配置文件中的属性注入到 bean 中。


Spring:
@Component 
@ComponentScan 
@Conditional 
@SpringBootApplication 是一个组合注解,它结合了以下三个注解的功能:
1. @SpringBootConfiguration: 表示这是一个Spring Boot配置类,它本质上是一个@Configuration注解,用于定义配置类,可以包含多个@Bean注解的方法。
2. @EnableAutoConfiguration: 告诉Spring Boot基于类路径设置、其他bean和各种属性设置来添加bean。例如,如果你添加了spring-webmvc和thymeleaf的依赖,这个注解就会自动配置你的应用程序为一个web应用程序。
3. @ComponentScan: 告诉Spring在包及其子包下扫描注解定义的组件(如@Component, @Service, @Repository等)。

aop在项目中有没有使用?aop使用的一些注解及其功能

一定要描述项目场景,web使用aop打印操作日志、使用aop做数据脱敏(150***8786)
过滤器是Servlet技术的一部分,它是Java EE规范的一部分
拦截器是Spring MVC框架的一部分,用于在处理HTTP请求时拦截控制器方法调用。
AOP底层是动态代理设计模式,在理论上效果在一定程度上相同
过滤器拦截器一般拦截某个web的前后,在controller执行前后
AOP是万物皆可拦截、甚至接口和类都可以切,可以增强controller、service、mapper……

定义一个切面类 @Aspect 声明为切面类 + @Component
定义切点 @Pointcut 声明切点表达式

eg:@AfterReturning(pointcut = “execution(public String com.example.yourpackage.Controller.*(..))”, returning = “result”)

通知
  • 前置 @Before
  • 后置 @After
  • 返回 @AfterReturning
  • 异常 @AfterThrowing
  • 环绕 @Around

💬 有没有在项目中使用 AOP?用来做什么?

有使用,主要应用场景有两个:

  • 一是 操作日志记录:在 Web 项目中,我们通过 AOP 拦截 Controller 层方法,在方法执行后记录操作人、操作类型、接口路径等信息,用于审计和排查问题。
  • 二是 数据脱敏处理:在某些返回值中包含手机号、身份证等敏感字段,通过 AOP 拦截 Service 返回结果,在返回给前端前进行统一脱敏(如:150***8786)。
@Aspect
@Component
public class LogAspect {

    // 定义切点,拦截所有 controller 包下的方法
    @Pointcut("execution(* com.example.controller..*(..))")
    public void controllerPointcut() {}

    // 环绕通知
    @Around("controllerPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("接口耗时:" + (end - start) + "ms");
        }
    }
}

🧠 AOP 通知类型简述(答题建议)

通知类型 注解 说明
前置通知 @Before 在方法执行前执行
后置通知 @After 方法执行后(无论成功或异常)
返回通知 @AfterReturning 方法成功返回后执行,可获取返回值
异常通知 @AfterThrowing 方法抛出异常后执行
环绕通知 @Around 方法执行前后都可执行,适合统一处理(如 try…finally)

你在项目中遇到的 AOP 注意事项

  • 如果你在 AOP 中使用 ThreadLocal(如存储登录用户信息),一定要在方法执行完后调用 remove() 释放资源,否则可能造成内存泄漏
  • 推荐使用 @Around + try-finally 模式,无论是否抛异常,finally 都能执行,比如清理上下文。
  • @AfterReturning仅在方法成功返回时执行不会处理异常情况,这一点面试常考。
  • AOP 是基于 动态代理机制(JDK 或 CGLIB),本质是对方法的增强。和拦截器、过滤器最大的不同是:AOP 不是仅限于 HTTP 请求,可以增强任意 Bean 的任意方法。

你在公司里负责的内容

想在controller访问完之后,想在aop实现之后再进行操作

UserThreadLocal 在执行完之后要 remove 出去,抛异常也会执行
@After 不管有无异常都会执行
@Around 结合try…catch…finally 里也可以达到同样效果

@After:这个注解用于定义一个通知(Advice),它在目标方法执行之后执行,无论目标方法执行的结果如何(成功或异常)。

@Aspect
@Component
public class AroundFinallyAspect {

    // 定义切点
    @Pointcut("execution(* com.example.yourpackage.controller..*(..))")
    public void controllerMethods() {
    }

    // 环绕通知
    @Around("controllerMethods()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            // 在目标方法执行之前执行
            result = joinPoint.proceed(); // 执行目标方法
            // 在目标方法成功执行之后执行
        } catch (Throwable e) {
            // 在目标方法抛出异常时执行
            throw e; // 可以选择处理异常或者重新抛出
        } finally {
            // 无论目标方法是否成功执行或者是否抛出异常,这里的代码都会执行
            performFinallyAction();
        }
        return result;
    }

    private void performFinallyAction() {
        // 在这里放置最终要执行的代码
    }
}

--------------------------------------------------------------------------------
// 后置通知
    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice(JoinPoint joinPoint) {
        // 在目标方法执行之后执行的逻辑
    }

    // 返回后通知
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        // 在目标方法成功执行并返回结果后执行的逻辑
    }

在第一个例子中,你直接在@After注解中指定了切点表达式,因此不需要额外的pointcut属性。

对于@AfterReturning注解,它不仅需要在目标方法执行之后执行通知,还需要访问目标方法的返回值。因此,@AfterReturning注解有一个额外的pointcut属性,用于指定切点表达式。此外,@AfterReturning注解还有一个returning属性,用于指定一个参数名,该参数将接收目标方法的返回值

@AfterReturning:在方法执行后返回结果后执行通知。【如果有异常不会处理】

我来帮你通俗一点梳理这段 AOP 的使用逻辑和运行机制,你可以类比成“程序在运行到某个点的时候,会自动执行你预设的增强代码”。

✅ 举个例子你就明白了:

你写了个 Controller:

@RestController
public class TestController {
    @GetMapping("/hello")
    public String hello() {
        System.out.println("正在执行业务逻辑");
        return "Hello World";
    }
}

⚙️ 当你访问 /hello,Spring 执行流程如下:

步骤 内容
1️⃣ 执行你的 AOP 中 @Around 方法(aroundAdvice)
2️⃣ joinPoint.proceed() 触发目标方法(即 hello())执行
3️⃣ 方法返回或抛异常,finally 一定会执行
4️⃣ AOP 把最终返回值交回 Spring 继续响应前端

✅ 补充理解:execution 表达式

你这个表达式:

execution(* com.example.yourpackage.controller..*(..))
  • *:匹配所有返回值类型
  • com.example.yourpackage.controller..*:匹配该包及子包的所有类
  • *:匹配所有方法
  • (..):匹配任意参数类型和个数

🧠 所以可以理解成:**”凡是 controller 包下的任何方法,AOP 都要插手”**

你就是告诉 Spring AOP:“当你运行到这些方法时,先来找我执行一些额外操作”,这就是切面增强的精髓

你们这个项目怎么技术选型的

我进到项目中很多已经确认下来的 一般由组长确定了

那你自己怎么想的?[开放性问题]

选xxx技术 网上资料/备书 比较多可以参考[用的人多]
学习成本[框架厉害但上手复杂不好用]
社区的活跃度[官网持续更新版本 框架会不断发展]

你处于后端的什么位置

初级 中级 高级
中级 骨干开发位置[协助组长完成]
中高级 完成设计类

你对你的职业规划是什么

讲实际的话
想成为高级开发/某个领域的专家
提前了解公司领域,有备而来

java基本类型

image-20241222141950707

short 可以占两个字节 可以用16位
int 可以占四个字节 -21亿 ~ 21亿
long 可以占八个字节 天文数字
float 可以占四个字节 0.2F/f
double 可以占八个字节 0.2D/d

float double尽量不要进行运算 ,在Java中进行金钱运算时,应当特别注意浮点数的精度问题,因为浮点数(如floatdouble)在表示某些数值时可能会丢失精度,这对于需要精确计算的金融计算来说是非常不合适的。

1.金钱转成分 向下取整
2.BigDecimal

ASCII码占1个字节 → Unicode字符占2个字节【有些汉字存不了】→ UTF-8占1-3个字节[灵活可变]
在我们性能中一般走Unicode编码性能更高一点 在网络中/存入磁盘Unicode转成ASCII码

jdk?之后 jdk开始存储大量英文和数字 String类也作了更新 不是基于基本数据类型 而是基于byte数组

在Java的早期版本中(例如JDK 1.4及之前版本),String类内部确实使用char数组来存储字符串数据。每个char在Java中占用16位(2个字节),这意味着不管存储的是英文字符还是数字,每个字符都会占用2个字节的内存空间。

从JDK 5开始,Java平台引入了一些变化,但String类的内部表示仍然基于char数组。直到JDK 6和JDK 7,String类的内部表示并没有改为基于byte数组。

真正发生变化的是在JDK 9中,String类内部表示从char数组转变为byte数组加上一个编码标识(coder),这种改变是为了更有效地存储只有ASCII字符的字符串。ASCII字符只需要一个字节来表示,因此使用byte数组可以节省内存空间。当字符串包含Unicode字符时,String类可能会使用更多的编码方式,例如LATIN1或UTF-16。

Vue的生命周期

生命周期的八个阶段:每触发一个生命周期事件,会自动执行一个生命周期方法(钩子)

  1. beforeCreate创建前
  2. created创建后
  3. beforeMount载入前
  4. mounted挂载完成
  5. beforeUpdate数据更新前
  6. updated数据更新后
  7. beforeUnmount组件销毁前
  8. unmounted组件销毁后

String是基础类型吗

不是,是java.lang下的类

String 在 Java 中并不是基础类型,而是一个引用类型。因为 String 是一个类,所以它是引用类型,意味着当我们声明一个 String 变量时,你实际上是指向一个 String 对象的引用

String 的特性
不可变性:String 对象一旦创建就不能被修改。任何改变 String 内容的操作都会创建一个新的 String 对象。
线程安全:由于 String 的不可变性,它们是线程安全的,可以自由地在多个线程之间共享。
字符串池:为了提高性能和减少内存使用,Java 为 String 提供了字符串常量池(String Pool)。当创建一个新字符串时,如果字符串池中已经存在相同内容的字符串,则会返回池中的实例,而不是创建新的对象。

java集合中list和set的区别?

都是接口 某个实现类

单链 有顺序 可重复 有索引[有下标]
单链 不可重复 无索引[无下标] 不能说是无序 因为TreeSet有序 HashSet就是无序的

做了几年开发呢? 实际几个项目?

三年[初中级] → 四~五个项目

你觉得敲代码最重要的是什么?

理解需求前期设计工作[数据库、接口 → 流程图(思路清晰)]、编码阶段[考虑方法封装、注释、考虑代码后期和维护性(设计模式 → 可维护性+扩展性)]、编码风格[阿里巴巴规范]

你的项目有上线吗? 多少人进行开发? 你主要负责后端吗?

有,介绍一下项目组成结构,是的[再问再回答]

SpringBoot的自动装配原理[启动过程中的一部分]SpringBoot启动原理&&如何内嵌外部原件

Spring Boot的自动装配原理是基于Spring框架的IoC(控制反转)和DI(依赖注入)的核心概念,并结合了一系列的约定和条件注解来实现配置类的自动加载和Bean的自动注册

  1. 启动类:Spring Boot 应用通常有一个带有 @SpringBootApplication 注解的启动类。这个注解是一个组合注解,它包含了 @Configuration@EnableAutoConfiguration@ComponentScan
  2. @EnableAutoConfiguration:这个注解是自动装配的关键。它告诉 Spring Boot 根据类路径下的类、Bean 的定义以及各种属性设置,自动配置 Spring 应用。这个注解会导入 AutoConfigurationImportSelector 类,该类会读取所有 spring.factories 文件中的 EnableAutoConfiguration 条目,并将它们作为配置类导入。
  3. 条件化配置:Spring Boot 使用 @Conditional 注解及其一系列的派生注解(如 @ConditionalOnClass@ConditionalOnMissingBean 等)来确保只有在满足特定条件时,配置类或 Bean 才会被创建。
  4. 配置类:自动装配是通过一系列的配置类来实现的,这些配置类包含了 @Bean 方法,用于创建和配置 Spring 容器中的 Bean。
自定义Starter
<!-- Maven项目的依赖示例 -->
<dependency>
    <groupId>com.xxx</groupId>
    <artifactId>xxx-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

三大优点:依赖Maven特性[依赖传递] 自动配置 内嵌Tomcat
Spring Boot的自动装配原理是
src/main/resources/META-INF目录下创建spring.factories文件,添加自动配置类的全限定名
我们可以在-info定义spring.factories位于META-INF目录下,Spring Boot使用它来发现和加载自动配置类。

配置类扫描: 通过@SpringBootApplication注解,Spring Boot会触发对@EnableAutoConfiguration注解的处理,该注解会查找spring.factories文件中定义的自动配置类。

Maven里面写test类 用configuration声明 写很多的test类 但是我可以自己写test类然后调不同的方法 应该怎么办?@Conditional[Spring的注解] → 做成非常灵活的 如果没有就用自己写的

Spring里面的事务传播行为

在Spring框架中,事务传播行为定义了事务方法之间的调用关系,即一个事务方法被另一个事务方法调用时,事务应该如何传播。

  1. REQUIRED(默认值) required
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. SUPPORTS supports
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
  3. MANDATORY mandatory
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  4. REQUIRES_NEW requires_new
    • 创建一个新的事务,如果当前存在事务,则挂起当前事务。
  5. NOT_SUPPORTED not_supported
    • 以非事务方式执行操作,如果当前存在事务,则挂起当前事务。
  6. NEVER never
    • 以非事务方式执行,如果当前存在事务,则抛出异常。
  7. NESTED nested
    • 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则行为类似于REQUIRED

REQUIRED:通常用于方法需要在一个事务中运行,但如果已经有一个事务在运行,那么它应该加入这个事务。
SUPPORTS:用于方法不需要事务上下文,但如果已经在一个事务中,它也可以在这个事务中运行。
MANDATORY:用于方法必须在事务中运行,如果没有事务,则会抛出异常。
REQUIRES_NEW:用于方法必须在自己的新事务中运行,即使当前已经有一个事务在运行。
[一般适用于不管有没有抛出异常 都要记录某些操作日志 不能在同一个类里底层是动态代理]
[如果a()和b()方法在同一个类中,并且a()直接调用b(),那么Spring的事务代理无法拦截这个内部调用,因此b()的REQUIRES_NEW事务传播行为不会生效。这是因为内部方法调用不会通过代理,而是直接在同一个对象实例上调用。]

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;

@Service
public class MyService {

@Autowired
private MyService self; // 注入自身代理实例

public void methodA() {
// ... 业务逻辑 ...

self.methodB(); // 通过代理实例调用,事务注解将生效
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// ... 业务逻辑 ...
}
}

NOT_SUPPORTED:用于方法不应该在事务中运行,如果有一个事务在运行,它将被挂起。
NEVER:用于方法绝对不应该在事务中运行,如果有一个事务在运行,将抛出异常。
NESTED:用于方法应该在嵌套事务中运行,嵌套事务可以独立于外部事务进行提交或回滚

Spring 事务传播行为对比表

传播行为 是否加入当前事务 当前无事务是否新建事务 当前有事务是否挂起原事务 常见用途 / 场景举例
REQUIRED(默认) ✅加入 ✅新建 ❌不挂起 大多数业务逻辑,默认即可(如用户下单)
SUPPORTS ✅加入 ❌不新建,直接运行 ❌不挂起 非核心操作,事务可有可无(如查询、打日志、非敏感接口)
MANDATORY ✅加入 ❌不新建 → 报错 ❌不挂起 强依赖事务环境,如在已有事务内强制做某个操作(如审计组件)
REQUIRES_NEW ❌不加入 ✅新建 ✅挂起原事务 日志记录、补偿事务等。如主逻辑回滚了,日志也必须写入数据库
NOT_SUPPORTED ❌不加入 ✅以非事务运行 ✅挂起原事务 第三方接口调用、不能用事务的场景(如读库压力大,必须立即返回)
NEVER ❌不加入 ✅以非事务运行 ❌不挂起 → 如果有事务 → 报错 严禁事务场景,例如你调用了一个只能在无事务环境下工作的接口
NESTED ✅作为嵌套事务 ✅新建(嵌套) ❌不挂起 子事务失败可以单独回滚但主事务不影响(例如更新多个模块,一个失败不影响其他)

🧠 快速记忆技巧

记忆关键词 说明
REQUIRED 就像“我有就用,没有我自己建” → 默认值
REQUIRES_NEW “谁都有我不管,我自己开新号”
SUPPORTS “随缘,有就上,没有就裸跑”
MANDATORY “必须有事务,没有就不干”
NOT_SUPPORTED “你们开会我不参与,我要单干”
NEVER “你们别开会,我一开会就炸”
NESTED “我是你肚子里的小事务,单独受罚,妈你别哭”

💬 示例记忆法(像讲段子一样):

老板(事务A)叫你干活(方法B):

  • REQUIRED:老板已经在开会,我就进来一起开;没会议我就自己组织一个
  • REQUIRES_NEW:我不管老板在不在,我自己重新开一个会(挂起老板的)
  • SUPPORTS:老板开会我就进,不开我就随便干干
  • MANDATORY:老板必须先开会,否则我不干活(抛异常)
  • NOT_SUPPORTED:老板要是开会,我就去抽根烟;不开我才来干活(挂起事务)
  • NEVER:谁开会我跟谁急,我只干没有事务的活
  • NESTED:老板开了会,我在会里又开了一个子会,出了事自己担责不连累老板

✅ 重点注意(常考陷阱)

场景 考点
同一个类中 a() 直接调用 b()(b有事务) 事务注解失效!因为 Spring 是基于代理的,自己调自己不走代理!解决:注入自己 @Autowired private Self self; 然后 self.b()
为什么要用 REQUIRES_NEW? 防止主事务回滚时影响某些必须成功的操作(如操作日志、发送消息)
NESTED 与 REQUIRES_NEW 区别? NESTED 依赖原事务,提交回滚可以局部控制;REQUIRES_NEW 完全独立,互不干扰
使用 NOT_SUPPORTED/NEVER 的典型场景? 调用第三方接口不允许事务干扰,如防止长事务锁表等问题

用过Spring的事务吗

一组数据库的增删改操作
声明式事务管理:这是Spring推荐的用法,它通过使用注解(如@Transactional)或基于XML的配置来声明事务边界。底层基于AOP实现动态代理增强方法
编程式事务管理:允许你通过编程的方式直接管理事务,通常使用TransactionTemplate或者直接使用底层的PlatformTransactionManager

声明式事务管理(@Transactional)

// 你需要在 Spring 配置中启用事务注解支持:
@Configuration
@EnableTransactionManagement
public class SpringConfig {

 @Bean
 public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
     return new JpaTransactionManager(entityManagerFactory);
 }

 // ... 其他配置
}


// 然后,你可以在服务层的方法上使用 @Transactional 注解来声明事务边界:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

 @Autowired
 private AccountRepository accountRepository;

 @Transactional
 public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
     // 执行转账操作,比如:
     Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(...);
     Account toAccount = accountRepository.findById(toAccountId).orElseThrow(...);

     fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
     toAccount.setBalance(toAccount.getBalance().add(amount));

     accountRepository.save(fromAccount);
     accountRepository.save(toAccount);

     // 如果这里发生异常,Spring 将回滚事务
 }
}

编程式事务管理

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class AccountService {

 @Autowired
 private TransactionTemplate transactionTemplate;

 public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
     transactionTemplate.execute(new TransactionCallbackWithoutResult() {
         @Override
         protected void doInTransactionWithoutResult(TransactionStatus status) {
             try {
                 // 执行转账操作,与声明式事务管理中的操作相同
                 // ...
             } catch (Exception e) {
                 status.setRollbackOnly(); // 如果发生异常,标记事务回滚
             }
         }
     });
 }
}

✅ 一句话理解

Spring事务 = 帮你保证一组数据库操作要么全部成功,要么全部失败(ACID)。


🌟 Spring事务使用方式

类型 特点 使用场景举例
声明式事务(@Transactional) 推荐方式,简洁,基于AOP代理增强方法 99%业务场景,转账、下单、库存扣减
编程式事务(TransactionTemplate) 自己控制事务开始、提交、回滚流程 特殊场景需要精细控制事务,如代码逻辑复杂、异常处理嵌套

🎯 实战答题模板

“在项目中我主要用的是声明式事务,通过@Transactional注解来管理事务,通常放在service层方法上。比如用户下单时,我需要在一个事务中完成订单生成、库存扣减、用户积分变更,如果中间任何一环失败,整个事务都会自动回滚,确保数据一致性。”

“我还了解过编程式事务,像使用TransactionTemplate手动管理事务边界,适合一些流程比较复杂或需要细粒度事务控制的业务场景。”

@Transactional 注解常用属性:

属性 说明 示例
propagation 事务的传播行为(REQUIRED/REQUIRES_NEW等) @Transactional(propagation=...)
rollbackFor 哪些异常会触发回滚(默认只回滚运行时异常) @Transactional(rollbackFor=Exception.class)
readOnly 是否只读事务,优化性能 @Transactional(readOnly=true)
timeout 超时时间,单位:秒 @Transactional(timeout=5)

💥 易错点提示(面试常问)

问题点 正确做法
注解加在controller层 ❌ → @Transactional应加在service层
方法内自己调用自己 ❌ 事务失效!应通过注入自己调用
只抛了Exception没加rollbackFor 默认只回滚RuntimeException
数据库引擎不是InnoDB MySQL需用InnoDB才支持事务

🔧 小结:项目中事务用法模版

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private StockMapper stockMapper;

    @Transactional(rollbackFor = Exception.class) // 推荐加上 rollbackFor
    public void createOrder(Order order) {
        // 1. 创建订单
        orderMapper.insert(order);

        // 2. 扣减库存
        stockMapper.deduct(order.getProductId(), order.getQuantity());

        // 3. 模拟异常测试事务回滚
        // int i = 1 / 0;

        // 4. 插入日志等其他操作...
    }
}

你熟悉的技术栈有哪些,用了哪些?

SpringBoot Vue Git Maven MyBatis……

解决难点的决策有和用户沟通的吗?

有过沟通 让他们了解一下我们的方案

万一用户听不懂怎么办?

我会用一些更加直白通俗的语言让用户理解我们的方案

用户不接受这个方案怎么办?

我们可以提供不止一个方案 或者 提供他提的方案 综合一下各种方案告诉其优缺点(站在我们的专业角度意见)和风险 让客户明知 让其选择

通常一般怎么学习的?最近在研究什么技术?

想面试的目的 要給公司带来一些好的
一般以公司的实际项目中为切入点去学习会更有效率

学习一个新的技术大概要多久?

1-2天 首先看官网 这个技术是解决哪些 看我们的项目需要哪些技术切入点能引用 然后去专门针对这个技术功能点去学习快速上手的接口文档

redis为什么这么快?

  • 主数据基于内存操作
  • Redis是单线程[操作数据的线程],避免上下文的频繁切换整个redis不是就一个线程
  • 底层基于C语言实现 得益于底层良好的数据结构[]
  • 基于非阻塞的IO提升IO读写性能,NIO,BIO,AIO…
Java四大杀手

集合数据结构 jvm 并发编程 网络IO

非阻塞IO(Non-blocking I/O)是一种IO模型,它允许程序在执行IO操作时不会被阻塞,即程序可以在发起IO请求后继续执行其他任务,而不需要等待IO操作完成。以下是关于非阻塞IO的一些关键点:

非阻塞IO的特点:

  1. 异步操作:非阻塞IO操作通常是异步的,意味着程序发起IO请求后,不需要等待IO操作完成,而是可以立即返回去做其他事情。
  2. 事件驱动:非阻塞IO往往与事件驱动模型结合使用,程序可以通过监听器来响应IO事件(如数据可读、连接可写等)。
  3. 减少等待时间:由于程序在等待IO操作完成时不会阻塞,它可以继续处理其他任务,从而提高了程序的响应性和吞吐量。

非阻塞IO的实现方式:

  • NIO(New I/O):在Java中,NIO提供了一种非阻塞的IO方式,使用Selector来管理多个通道(Channel)上的IO事件。
  • AIO(Asynchronous I/O):AIO是另一种非阻塞IO模型,它允许程序完全异步地执行IO操作,通常是通过完成端口(Completion Ports)来实现。
  • BIO(Blocking I/O):与非阻塞IO相对的是阻塞IO,其中每个IO操作都会阻塞调用线程,直到操作完成。

非阻塞IO的优势:

  • 资源利用率:非阻塞IO可以更有效地利用系统资源,因为单个线程可以处理多个IO操作。
  • 高并发处理:在处理大量并发连接时,非阻塞IO可以显著提高系统的并发处理能力。

非阻塞IO在Redis中的应用:

  • 单线程模型:Redis是一个基于内存的键值存储数据库,它使用单线程模型来处理所有客户端请求。由于操作是基于内存的,速度非常快,而单线程避免了上下文切换的开销。
  • 非阻塞IO和多路复用:尽管Redis是单线程的,但它使用非阻塞IO和多路复用技术(如epoll或kqueue)来同时处理多个IO流。这意味着Redis可以在等待IO操作(如网络响应)时不阻塞,从而可以继续处理其他请求。
  • 高性能:Redis的非阻塞IO和多路复用机制使得它即使在面对大量并发请求时也能保持高性能。

redis的数据类型以及使用场景分别是什么

写入依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version> <!-- Use the latest version available -->
</dependency>
  • String:存储对象信息(转JSON)
    将用户信息序列化为JSON字符串后存储。

    SET user:1000 '{"name":"Alice","age":30,"email":"alice@example.com"}'
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisStringExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.set("user:1000", "{\"name\":\"Alice\",\"age\":30,\"email\":\"alice@example.com\"}");
            String userInfo = jedis.get("user:1000");
            System.out.println(userInfo);
            
            jedis.incr("visits");
            long visits = jedis.get("visits");
            System.out.println("Visits: " + visits);
            
            jedis.close();
        }
    }
    
  • List:链表,查询记录的缓存、列表,朋友圈,微博,队列数据结构
    可以将数据库查询结果缓存为一个列表

    # 查询记录的缓存
    LPUSH recent:queries "SELECT * FROM users WHERE age > 30"
    
    # 消息队列
    使用List作为消息队列,生产者将消息LPUSH到列表,消费者从列表中RPOP消息
    LPUSH message:queue "message1"
    RPOP message:queue
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisListExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.lpush("recent:queries", "SELECT * FROM users WHERE age > 30");
            String query = jedis.rpop("recent:queries");
            System.out.println("Recent Query: " + query);
            
            jedis.close();
        }
    }
    w
    
  • Hash:获取局部属性,小key不能设置过期时间Hash是一个键值对集合,适合存储对象
    Hash是一个键值对集合,适合存储对象

    HSET user:1000 name "Alice" age 30 email "alice@example.com"
    HGET user:1000 name
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisHashExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.hset("user:1000", "name", "Alice");
            jedis.hset("user:1000", "age", "30");
            jedis.hset("user:1000", "email", "alice@example.com");
            
            String name = jedis.hget("user:1000", "name");
            System.out.println("Name: " + name);
            
            jedis.close();
        }
    }
    
  • Set:无序不可重复的,收藏,点赞,社交场景,聚合计算(∩∪差集)
    社交场景:使用Set来存储用户的关注列表,确保关注关系的唯一性。

    SADD user:1000:following 2000 3000 4000
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisSetExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.sadd("user:1000:following", "2000", "3000", "4000");
            Set<String> following = jedis.smembers("user:1000:following");
            System.out.println("Following: " + following);
            
            jedis.close();
        }
    }
    

    聚合计算:计算两个用户的共同关注。

    SINTER user:1000:following user:2000:following
    
  • Zset:排序场景,排行榜,姓名排序

    排行榜:根据用户得分来存储排行榜

    ZADD leaderboard 1000 Alice 950 Bob 900 Charlie
    ZRANGE leaderboard 0 -1 WITHSCORES
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    import java.util.Set;
    
    public class RedisZsetExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.zadd("leaderboard", 1000, "Alice");
            jedis.zadd("leaderboard", 950, "Bob");
            jedis.zadd("leaderboard", 900, "Charlie");
            
            Set<String> leaderboard = jedis.zrange("leaderboard", 0, -1);
            System.out.println("Leaderboard: " + leaderboard);
            
            jedis.close();
        }
    }
    

    姓名排序:存储学生姓名和成绩,并按成绩排序。

    ZADD students 92 John 85 Mary 88 Alice
    ZRANGE students 0 -1 WITHSCORES
    

分布式锁都可以用。Redisson是Redis的儿子,底层为Hash

redis数据过期策略

  • 惰性删除:键过期时不会立即删除,当访问该键时判断是否过期,如果过期就删除
    惰性删除策略是在访问键时检查键是否过期,如果过期则删除。

    import redis.clients.jedis.Jedis;
    
    public class LazyExpiration {
        private Jedis jedis;
    
        public LazyExpiration() {
            // 连接到Redis服务器
            this.jedis = new Jedis("localhost");
        }
    
        public String getKey(String key) {
            // 检查键是否存在
            if (!jedis.exists(key)) {
                return null;
            }
    
            // 检查键是否过期
            if (isExpired(key)) {
                // 如果键已过期,则删除它
                jedis.del(key);
                return null;
            }
    
            // 如果键未过期,返回键的值
            return jedis.get(key);
        }
    
        private boolean isExpired(String key) {
            // 获取键的剩余生存时间,如果返回值大于0,则键未过期
            return jedis.ttl(key) == -2;
        }
    
        public static void main(String[] args) {
            LazyExpiration lazyExpiration = new LazyExpiration();
            String value = lazyExpiration.getKey("myKey");
            if (value != null) {
                System.out.println("Key value: " + value);
            } else {
                System.out.println("Key does not exist or has expired.");
            }
        }
    }
    
  • 定时删除:设置键的过期时间,当键过期时,立即删除

    import redis.clients.jedis.Jedis;
    
    public class ActiveExpiration {
        private Jedis jedis;
    
        public ActiveExpiration() {
            // 连接到Redis服务器
            this.jedis = new Jedis("localhost");
        }
    
        public void activeExpireCycle() {
            // 随机检查一定数量的键
            for (int i = 0; i < 10; i++) {
                String key = jedis.randomKey();
                if (key != null && isExpired(key)) {
                    // 如果键已过期,则删除它
                    jedis.del(key);
                }
            }
        }
    
        private boolean isExpired(String key) {
            // 获取键的剩余生存时间,如果返回值大于0,则键未过期
            return jedis.ttl(key) == -2;
        }
    
        public void runPeriodicTask() {
            // 定时任务,按照一定的频率运行
            while (true) {
                activeExpireCycle();
                try {
                    Thread.sleep(1000); // 每秒执行一次
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            ActiveExpiration activeExpiration = new ActiveExpiration();
            // 启动定时任务
            activeExpiration.runPeriodicTask();
        }
    }
    
高薪冲刺 → 定时删除详细策略

要扫描所有的定期任务删除 有策略可以设置阈值

啥时候离职的?半个月太长了

刚刚离职 也是刚刚开始投

主要工作职责

主要负责后端工作,协助测试,运维上线

你对前端有了解过吗?

有了解过,如HTML、CSS、JavaScript、框架[Vue、Element]等 可以很快的上手
我主要专长在于后端开发,可以学习和了解更多的前端知识

各种淘汰策略介绍

Redis提供了8种淘汰策略,可以分成两大类:

1、针对所有键的策略:对所有键进行选择和淘汰。

2、仅针对有过期时间的键的策略:只在设置了过期时间的键中选择淘汰对象。

以下具体策略:
可以区分为两类:[有设置过期时间的key 不管你有没有设置过期时间]

1. noeviction【默认】
  • 描述:达到内存限制时,不再执行删除操作,直接拒绝所有写入请求(包括插入和更新)。[可以读 但是拒绝写请求]
  • 适用场景希望数据永不丢失的场景,但需要保证内存充足,否则会导致写入操作失败。
2. allkeys-lru(最近最少使用)
  • 描述:在所有的键中使用 LRU算法,删除最近最少使用的键。
  • 适用场景:适合缓存场景,保留频繁访问的键,逐出很少被访问的键。
3. allkeys-lfu(最少使用频率)【电商】
  • 描述:在所有键中使用 LFU 算法,删除使用频率最低的键。
  • 适用场景:适用于需根据使用频率进行淘汰的场景,更关注访问次数而非访问时间。
4. volatile-lru(最近最少使用)
  • 描述:仅对设置了过期时间的键使用 LRU 算法。
  • 适用场景:适合缓存一些有过期时间的数据,希望根据访问频率来进行内存管理的场景。
5. volatile-lfu(最少使用频率)【电商】
  • 描述:仅对设置了过期时间的键使用 LFU算法。
  • 适用场景:同 volatile-lru,但更关注使用频率
6. allkeys-random
  • 描述:在所有键中随机选择删除某个键。
  • 适用场景:适用于缓存数据访问频率没有明显差异的情况。
7. volatile-random
  • 描述:在所有设置了过期时间的键中随机选择删除某个键。
  • 适用场景:适合缓存带有过期时间的数据,删除哪个数据不重要的场景。
8. volatile-ttl
  • 描述:在设置了过期时间的键中,优先删除剩余生存时间(TTL)较短的键。
  • 适用场景:适合希望优先清理即将过期的数据的场景。
import redis.clients.jedis.Jedis;

public class RedisMaxMemoryPolicyExample {
    private Jedis jedis;

    public RedisMaxMemoryPolicyExample() {
        // 连接到Redis服务器
        this.jedis = new Jedis("localhost");
    }

    public void setMaxMemoryPolicy(String policy) {
        // 设置Redis的内存淘汰策略
        jedis.configSet("maxmemory-policy", policy);
    }

    public String getMaxMemoryPolicy() {
        // 获取当前Redis的内存淘汰策略
        return jedis.configGet("maxmemory-policy").get(1);
    }

    public static void main(String[] args) {
        RedisMaxMemoryPolicyExample example = new RedisMaxMemoryPolicyExample();

        // 设置不同的内存淘汰策略
        example.setMaxMemoryPolicy("noeviction");       // 默认策略,拒绝写请求
        example.setMaxMemoryPolicy("allkeys-lru");      // 所有键使用LRU淘汰
        example.setMaxMemoryPolicy("allkeys-lfu");      // 所有键使用LFU淘汰
        example.setMaxMemoryPolicy("volatile-lru");     // 仅有过期时间的键使用LRU淘汰
        example.setMaxMemoryPolicy("volatile-lfu");     // 仅有过期时间的键使用LFU淘汰
        example.setMaxMemoryPolicy("allkeys-random");   // 所有键随机淘汰
        example.setMaxMemoryPolicy("volatile-random");  // 仅有过期时间的键随机淘汰
        example.setMaxMemoryPolicy("volatile-ttl");     // 优先淘汰TTL较短的键

        // 获取当前内存淘汰策略
        String currentPolicy = example.getMaxMemoryPolicy();
        System.out.println("Current Max Memory Policy: " + currentPolicy);
    }
}

缓存三兄弟(穿透、击穿、雪崩)

一般在读缓存的时候出现的问题。思路:产生的原因 + 解决的方案

==缓存穿透==:用户或前端查询到一个在数据库中不存在的数据,先查redis再走数据库。对数据库压力会很大。关系型数据库是性能的瓶颈 希望把高数量都挡在数据库前面。查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库(可能原因是数据库被攻击了 发送了假的/大数据量的请求url)

  • 解决方案一缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存 {key:1, value:null} 【空字符串】没有Null的数据类型下一次读取直接把空串返回
    优点:简单
    缺点:消耗内存,可能会发生不一致的问题

如果一直模拟一个不同的不存在的key 这时候就要用到布隆过滤器

  • 解决方案二布隆过滤器 (拦截不存在的数据)
    [商品读多写少上缓存,要把商品数据写到布隆过滤器中,以商品的id独一无二计算hashcode,用布隆过滤器。取模数组落到桶内 会把0置为1]
    有很多个二进制数组每个二进制数组用不同的hash算法进行计算此时落到的桶就不一样
    作用:读的时候 前端传id 之前怎么写进去就怎么拿出来。[位运算(与)速度很快 把多个数组的数据拿出来与运算如果都是1 则这个数据可能存在再查一遍缓存 若不存在直接return返回] 布隆过滤器说你不存在 一定不存在,说你存在 则可能存在[哈希冲突]

    ★ 项目上线很久了 商品早就下架不卖了 这时候会发生什么问题

    这时候布隆过滤器还有之前的痕迹,需要把那些1设置为0。
    布隆过滤器 不支持对某个的1设置0 → 因为有哈希冲突我不知道这个1曾经是誰设置的
    支持将整个都置为0,之后可以搞个定时任务
    布隆过滤器具体实现:Redis、Redission亲儿子、1cache、咖啡因(Caffeine提供了一种非常高效且易于使用的缓存解决方案,它支持多种缓存过期策略)、Guava谷歌

    ★ 以前没设置过且上架过 后面加了布隆,后面要把之前所有数据重新搞进去 怎么解决存量数据

    写一个定时任务

    ★ 场景:工商银行统计每天的用户日活量[上线就算] 要查询某个人连续七天签到 怎么查(用位图)用户量太多了

    搞一个二进制数组,10亿长度的数组,每个数组是一个bit = 10亿个位,一个字节1/bit=8个位,综合计算后大概消耗119MB的空间每天。用用户id去hash 如果用户登录将0置为1有单独的位图结构,统计时间就可以拿日期 往前面数 拿某个id去取模得到桶 找前七个,去进行与运算,连续为1就达到了重复连续七天前端。否则非连续七天。

    在缓存预热时,要预热布隆过滤器。根据id查询文章时查询布隆过滤器如果不存在直接返回

    import redis.clients.jedis.Jedis;
    
    public class DailyActiveUserCounter {
        private Jedis jedis;
    
        public DailyActiveUserCounter() {
            this.jedis = new Jedis("localhost"); // 连接到Redis服务器
        }
    
        // 映射用户ID到位图的键
        private String getUserBitmapKey(long userId, int day) {
            return "user:bitmap:" + userId + ":" + day;
        }
    
        // 用户签到
        public void userSignIn(long userId, int day) {
            String key = getUserBitmapKey(userId, day);
            jedis.setbit(key, userId % 86400, true); // 假设一天有86400秒,使用秒数作为偏移量
        }
    
        // 检查用户连续七天的签到情况
        public boolean checkContinuousSignIn(long userId, int day) {
            for (int i = 0; i < 7; i++) {
                String key = getUserBitmapKey(userId, day - i);
                if (jedis.getbit(key, userId % 86400) == false) {
                    return false; // 如果在连续的七天内有一天没有签到,则返回false
                }
            }
            return true; // 连续七天都有签到
        }
    
        public static void main(String[] args) {
            DailyActiveUserCounter counter = new DailyActiveUserCounter();
    
            // 假设用户ID为12345,今天签到
            long userId = 12345;
            int today = 1; // 假设今天是第1天
            counter.userSignIn(userId, today);
    
            // 检查用户是否连续七天签到
            boolean isContinuous = counter.checkContinuousSignIn(userId, today);
            System.out.println("User " + userId + " has signed in for 7 consecutive days: " + isContinuous);
        }
    }
    

    **bitmap(位图)巨大的二进制数组**:相当于一个以bit位为单位的数组,数组中每个单元只能存储二进制数0或1

    布隆过滤器作用:可以用于检索一个元素是否在集合中

    • 存储数据:id为1的数据,通过多个hash函数获取hash值,根据hash计算数组对应位置改为1
    • 查询数据:使用相同hash函数获取hash值,判断对应位置是否都为1

    存在误判率:数组越小 误判率越大 【要数组足够大 误判率就小】

    bloomFilter.tryInit(size, 0.05) //误判率5%
    

==缓存击穿==:给某一个热点key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能一瞬间把DB击穿微博[鹿晗+关晓彤]【并发同一时间访问】

  • 解决方案一互斥锁【数据强一致性 性能差 (银行)】[控制一个个来访问的次数]

    AQS、ReentrantLock是进程级别的互斥锁,因为有数据在节点1或节点2,分布式锁是在不同场景都可以锁也可以控制访问顺序。

    以商品id作为key 先redis开始查缓存 判断是否为空 不为空直接return后解锁,空就先加锁 去数据库查完备份一份redis后解锁。被锁的其他线程在外面等待。

    ★ 100个人访问同一个商品,只有一个抢到锁,剩下的99个人也要查redis缓存和数据库。

    方案:**双重缓存校验** 先查缓存 查不到加锁 再查缓存 查不到再去数据库 查完后看是否备份后解锁冷代码

    1.查询缓存,未命中 → 2.获取互斥锁成功 → 3.查询数据库重建缓存数据 → 4.写入缓存 → 5.释放锁

    1.查询缓存,未命中 → 2.获取互斥锁失败 → 3.休眠一会再重试 → 4.写入缓存重试 → 5.缓存命中

  • 解决方案二逻辑过期[ 不设置过期时间 ] 【高可用 性能优 不能保证数据绝对一致 (用户体验)】

    在数据库一条数据里面添加一个 “expire”: 153213455

    1.查询缓存,发现逻辑时间已过期 → 2.获取互斥锁成功 → 3.开启线程 ↓→ 4.返回过期数据

    ​ 【在新的线程】→ 1.查询数据库重建缓存数据 → 2.写入缓存,重置逻辑过期时间 → 3.释放锁
    1.查询数据缓存,发现逻辑时间已过期 → 2.获取互斥锁失败 → 3.返回过期数据

==缓存雪崩==:在同一个时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来压力

  • 解决方案一:给不同的key的TTL(过期时间)添加随机值不在同一时间过期
  • 解决方案二:利用Redis集群提高服务的可用性 【哨兵模式、集群模式】
  • 解决方案三:给缓存业务添加降级限流策略【nginx、springcloud、gateway】
  • 解决方案四:给业务添加多级缓存 【Guava(做一级缓存 然后Redis是二级缓存)或Caffeine】
★ redis宕机的时候 再RedisTemplate.set()后会报错 但是现在mysql还可以访问 应该怎么办?

try catch 在里面继续再去查mysql数据库

降级代码:对于读操作,如果Redis缓存失效,可以直接从MySQL数据库读取数据。
public boolean setData(String key, String value) {
    try {
        // 尝试将数据设置到Redis
        redisTemplate.opsForValue().set(key, value);
        return true;
    } catch (Exception e) {
        // 日志记录Redis错误
        log.error("Redis is down, failing over to MySQL", e);
        
        // Redis设置失败,降级到MySQL
        return setDataToMySQL(key, value);
    }
}

private boolean setDataToMySQL(String key, String value) {
    // 这里实现将数据写入MySQL的逻辑
    // 例如:
    // mySqlTemplate.update("INSERT INTO cache (key, value) VALUES (?, ?)", key, value);
    // 注意:这里的mySqlTemplate和SQL语句需要根据实际情况调整
    return true; // 假设写入成功
}
如果公司对要求更高,需要限流降级、熔断

同一时间设置QPS为100 超过的返回友好提示[商品太火爆啦,请稍后再试]

你有自己部署过环境吗

公司里面用 Jenkins + docker 测试环境我们部署 生产环境是组长部署

未来1-3年规划

将具体一点,从业务技术上提升自己的深度和广度达到高级工程师

你平时做笔记吗

有做笔记 Xmind + Markdown
因为我觉得无论从网上的还是别人请教的不经历我的消化都不是我的东西
我还是会将这些知识点总结起来变成自己的知识

什么是动态代理?&& 动态代理有哪些,他们之间的区别?

代理是一种设计模式 用来增强目标的逻辑 与被增强的并没有太大关系装饰者模式

在程序运行期间才会产生代理类加载到我们jvm中yaml文件

  • JDK动态代理是 基于接口实现来实现增强

    [txt文本 把目标增强类 作为接口本身就是接口 实现过来写成源码 源文件 再用jdk工具把源码编译成class字节码 再用类加载器把class加载到jvm中]

  • CGLIB动态代理是 基于继承目标类并覆写其方法来实现

    [ASN字节码机制直接生成class 直接加载到内存中]性能较高,速度更快。因为直接生成class

要调用某个方法 CGLIB性能高 是通过反射来实现的 老版本的jdk的反射性能较低。如今在调用方法的性能上差距不大

区别

  • JDK动态代理要求目标类必须实现一个或多个接口,而CGLIB没有这个要求。
  • JDK动态代理生成的代理类是接口的实现,而CGLIB生成的代理类是目标类的子类。
  • 性能上,CGLIB通常比JDK动态代理更快,因为它直接操作字节码生成新的类。

什么样的代码是静态代理?

发生在我们写代码的过程中 在编译阶段产生了代理类
静态代理是指代理类在编译时就已经确定,通常由程序员手动编写

你用过Linux吗?

是的,我在工作中经常使用Linux操作系统。我熟悉Linux的基本命令
基础的命令:xxx【查看之前笔记】

你工作的时候有需求文档吗?

有的,有一些简单的需求是没有的[沟通成本太高了]
稍微复杂的需求会有需求文档,我会根据需求文档来理解项目需求,并进行系统设计和开发。

你有什么需要了解的?不要难为面试官,不问技术栈

我想了解一下贵公司的业务是什么…好的那我这块已经没有什么想了解的了 感谢面试官
HR:想了解一下贵公司的上班时间…
我没有什么想了解的,来之前有了解过贵公司

平时用注解创建的bean是单例的还是多例的?

默认情况下,通过注解(如@Component、@Service、@Repository、@Bean等)创建的Bean是单例的。如果需要创建多例Bean,可以在注解上添加@Scope(“prototype”)来指定。

// 单例Bean
import org.springframework.stereotype.Component;

@Component
public class SingletonBean {
    // Bean的代码
}

----------------------------------------------------
    
// 多例Bean

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class PrototypeBean {
    // Bean的代码
}

SQL语句的执行顺序,为什么顺序是这样排的,这样的顺序有什么优势或者好处?

FROM -> Join -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> limit
这样的顺序是为了优化查询性能。首先确定数据来源(FROM),然后筛选出满足条件的数据(WHERE),接着进行分组(GROUP BY),在分组的基础上进行进一步筛选(HAVING),然后选择需要的数据(SELECT),最后对结果进行排序(ORDER BY)。这样的顺序可以减少中间结果集的大小,提高查询效率。

书写顺序

select -> from -> join -> on -> where -> group by -> having -> order by -> limit

SQL的“写法顺序” vs “执行顺序”

书写顺序(你写的时候) 实际执行顺序(数据库引擎执行时)
SELECT … FROM … FROM(确定数据源)
[JOIN … ON …] JOIN/ON(表连接)
WHERE … WHERE(行过滤)
GROUP BY … GROUP BY(分组)
HAVING … HAVING(组过滤)
SELECT … SELECT(列筛选/表达式)
ORDER BY … ORDER BY(排序)
LIMIT … LIMIT(分页/截断)

为什么执行顺序是这样设计的?(核心)

🌟1. 先FROM

确定要操作的表或视图,这是基础,后面所有步骤都基于这个“数据源”。

🌟2. JOIN/ON

在多个表之间构建连接关系,决定你从这些表中怎样拼接出一张“临时大表”

🌟3. WHERE

在JOIN后对临时表做行级过滤,只保留符合条件的记录。
👉 这样可以尽早“减小数据量”,提升后续处理效率(比如分组)

🌟4. GROUP BY

基于保留的数据按某个字段做分组聚合,如统计、求和等。

🌟5. HAVING

聚合后的组进行筛选(如:只保留总金额 > 1000 的组)。

🌟6. SELECT

选择返回哪些字段,同时也可能包含聚合函数(如 COUNT(*), SUM()),此时组已经存在了。

🌟7. ORDER BY

对最终结果排序,这个阶段才有机会看到完整结果。

🌟8. LIMIT

只取前几行,分页优化的最后一环,减少结果输出体积

SELECT u.username, SUM(o.amount) AS total_amount
FROM user u
JOIN `order` o ON u.id = o.user_id
WHERE o.status = 'paid'
GROUP BY u.id
HAVING total_amount > 0
ORDER BY total_amount DESC
LIMIT 5;

这样的顺序有什么优势?

阶段 目的/优势
FROM+JOIN 先构建大数据源结构,作为后续处理基础
WHERE 尽早过滤不必要的数据 → 提高性能(减少后续数据量)
GROUP BY 只对保留下来的数据做聚合,提高分组效率
HAVING 聚合后再过滤,避免误筛不完整组
SELECT 最后选择字段,保证表达式都合法且数据齐全
ORDER BY 排序通常开销大,放最后执行最合理
LIMIT 控制输出量,提高响应速度和用户体验

面试如何回答(建议模板)

“SQL的执行顺序设计是为了最大限度提升查询效率。先执行FROM和JOIN是因为必须先构建数据源;然后用WHERE尽早过滤行,避免无意义的数据进入分组、排序等更耗性能的阶段;GROUP BY在WHERE过滤后进行,HAVING则是对聚合结果再做筛选;SELECT最后才执行,是因为只有在前面都处理完后,才能确定哪些字段可用;ORDER BY 和 LIMIT是最终结果的加工。这种顺序是数据库执行优化的核心逻辑。”

线上项目发生死锁如何去解决? 我暂时没遇到过→分布式事务上去答

死锁:两个线程争夺两个资源的时候 1线程拿到a 想拿b 2线程拿到了b 想拿a
四个原因互斥条件 请求保持 不可剥夺 循环等待
产生死锁的四个因素 同时满足才会死锁 想要解决死锁 需要打破其中一个原因就行

  1. 互斥条件(Mutual Exclusion):资源不能被多个线程同时使用。即某个资源在一段时间内只能由一个线程占用,其他线程必须等待该资源被释放后才能使用。
  2. 持有和等待条件(Hold and Wait):线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
  3. 非抢占条件(No Preemption):已经分配给某个线程的资源在该线程完成任务前不能被抢占,即只能由线程自己释放。
  4. 循环等待条件(Circular Wait):存在一种线程资源的循环等待链,每个线程都在等待下一个线程所持有的资源。

在实际操作中,以下是一些打破死锁的具体方法:

  • 资源分配图:使用资源分配图来检测循环等待条件,并在检测到循环时采取措施。
  • 锁排序:确保所有线程以相同的顺序获取锁,从而避免循环等待。
  • 超时机制:线程在请求资源时设置超时时间,如果超过时间未获得资源,则放弃当前任务并释放已持有的资源。
  • 死锁检测算法:运行死锁检测算法,如银行家算法,来检测系统中的死锁,并在必要时采取措施。
  • 线程中断:允许系统或其他线程中断正在等待资源的线程。
  • 回滚操作:如果检测到死锁,可以让某些线程回滚它们的工作,并释放资源,从而打破死锁。

MySQL是不会有死锁的 自身会检测 [让后面的超时释放回滚]
在分布式事务 线程1拿着资源a是数据库1 线程2拿着资源b是数据库2
JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁

你可以使用jstack命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态

  1. 首先,找到你的Java进程ID(PID)。你可以使用jps命令来列出所有正在运行的Java进程及其PID。

    jps
    
  2. 使用jstack命令打印出该Java进程的堆栈跟踪。

    jstack <PID>
    

    <PID>替换为实际的进程ID。

  3. 查找堆栈跟踪中的”DEADLOCK”关键字。jstack会自动检测死锁并在输出中报告。

我们在生产环境中遇到接口卡住不响应的情况,会先使用 jps 找到目标 Java 进程的 PID,然后用 jstack <PID> 导出线程堆栈日志。我们重点查看是否存在 Found one Java-level deadlock 的提示,并分析具体是哪几个线程锁住了彼此的资源。如果确认是死锁,我们会进一步分析代码逻辑进行解耦或调整加锁顺序。

Found one Java-level deadlock:

“Thread-1”:
waiting to lock monitor 0x00000000053c3d28 (object 0x000000076b300030, a java.lang.Object),
which is held by “Thread-0”
“Thread-0”:
waiting to lock monitor 0x00000000053c3d48 (object 0x000000076b300028, a java.lang.Object),
which is held by “Thread-1”

Java stack information for the threads listed above:

Thread-1:
at DeadlockDemo.lambda$main$1(DeadlockDemo.java:20)

  • waiting to lock <0x000000076b300030> (a java.lang.Object)
  • locked <0x000000076b300028> (a java.lang.Object)

Thread-0:
at DeadlockDemo.lambda$main$0(DeadlockDemo.java:11)

  • waiting to lock <0x000000076b300028> (a java.lang.Object)
  • locked <0x000000076b300030> (a java.lang.Object)

如果你遇到(新的)技术栈,怎么去解决?

【return Previous.notes(NowDay);】

如果你在实际开发中遇到问题,你怎么去解决,怎么去沟通?

首先尝试自己解决问题,通过搜索引擎、官方文档、Debug等。
尽可能不让这个问题不出现风险 实在解决不了就向上反馈 寻求帮助 请教上司领导或同事
平常和项目经理进行沟通 如果需求评审有些不理解还是会及时沟通 不清楚的一定要及时明确

对于加班情况怎么看?

为了确保项目进度和团队利益,加班是可以接受的。

多线程怎么保证线程之间的安全

加锁 不让多线程抢夺资源

互斥锁、读写锁、线程局部存储(ThreadLocal每个线程独享自己变量)

mybatis中${}和#{}的区别,哪个更好? 为什么?

  1. ${}(字符串替换):
    • ${}会将参数直接替换到SQL语句中,不进行任何转义处理。
    • 它适用于动态SQL中的表名或列名,或者在SQL语句中需要使用特定数据库函数的情况。
    • 使用${}时,如果参数是用户输入的,那么可能会引发SQL注入攻击,因为它不会对参数进行转义。
  2. #{}(预处理语句参数):
    • #{}会创建预处理语句(prepared statement)的参数占位符,并在设置参数时进行适当的转义处理。
    • 它适用于大部分情况,特别是当参数是用户输入时,可以有效防止SQL注入攻击。
    • MyBatis会根据参数的类型自动选择setStringsetIntsetDate等预处理语句方法。
  3. 在大多数情况下,#{}是更好的选择,因为它提供了以下优势:
    • 安全性#{}可以防止SQL注入攻击,因为它会自动转义参数
    • 类型处理:MyBatis会根据参数的实际类型来设置预处理语句的参数,这减少了类型错误的可能性。
    • 可读性和维护性:使用#{}可以使SQL语句更加清晰,因为它清楚地标识了参数的位置。

    然而,在某些特定的场景下,如需要动态地指定表名或列名时,${}是必要的,因为预处理语句

    MyBatis 中 #{}${} 的区别

    项目 #{}(占位符) ${}(字符串拼接)
    替换方式 使用 ? 占位符 字符串直接拼接进SQL
    是否转义 ✅ 自动转义,安全 ❌ 不转义,有SQL注入风险
    使用底层 JDBC 的 PreparedStatement 字符串拼接,执行前已成完整SQL
    场景 传入值如:用户名、ID、价格等参数 动态表名、列名、排序字段、函数等
    安全性 安全,推荐使用 不安全,谨慎使用
    类型处理 ✅ 自动处理类型转换 ❌ 不做类型处理,需手动拼接格式
    性能(缓存) 支持 SQL 缓存 由于 SQL 变化大,缓存命中低

    正确使用 #{}

    <select id="getUser" resultType="User">
      SELECT * FROM users WHERE id = #{userId}
    </select>
    
    • 自动转为:SELECT * FROM users WHERE id = ?
    • 安全,可防止 SQL 注入。

    必须使用 ${} 的场景(动态表名或列)

    <select id="selectByColumn" resultType="User">
      SELECT * FROM users WHERE ${columnName} = #{value}
    </select>
    
    • 用于:动态字段(如 nameemail)查询。
    • 注意:${columnName} 可能来自用户输入,必须白名单校验,否则容易被 SQL 注入。

说一下内连接和外连接的区别

左外连接(Left Outer Join)

  • 定义:左外连接返回左表中的所有行,即使在右表中没有匹配的行。对于左表中没有匹配的行,结果集中的右表部分将包含NULL。
  • 如果左表是主表,或者左表中的数据是查询的主要关注点,而右表中的数据是辅助信息时,通常使用左外连接。

右外连接(Right Outer Join)

  • 定义:右外连接返回右表中的所有行,即使在左表中没有匹配的行。对于右表中没有匹配的行,结果集中的左表部分将包含NULL。
  • 如果右表是主表,或者右表中的数据是查询的主要关注点,而左表中的数据是辅助信息时,通常使用右外连接

全外连接(Full Outer Join)

  • 定义:全外连接返回左表和右表中的所有行。当某行在另一个表中没有匹配时,结果集中的相应部分将包含NULL。
  • 全外连接不常用,因为它通常会返回大量的包含NULL的数据,这可能会导致查询结果难以解释。只有在确实需要两表中的所有数据时才使用。

  • 性能考虑:外连接可能会比内连接(Inner Join)更消耗资源,特别是当表很大时。如果可能,尽量使用内连接。
  • 数据完整性:如果业务逻辑要求查询结果必须包含某个表的所有记录,那么应该使用相应的外连接。

内连接 vs 外连接:核心区别总结

类型 说明 是否保留非匹配行 NULL 出现在哪一边
内连接 只返回两个表中满足连接条件的交集部分 ❌ 不会有 NULL
左外连接 保留左表所有记录,右表没有匹配的填 NULL ✅(保留左表) 右表字段为 NULL
右外连接 保留右表所有记录,左表没有匹配的填 NULL ✅(保留右表) 左表字段为 NULL
全外连接 左右两表都保留,不匹配的部分填 NULL(并集 ✅(都保留) 两边都可能为 NULL

✅ 图示记忆法

  • A 表在左边,B 表在右边:
    INNER JOIN:     A ∩ B
    LEFT JOIN:      A ⟕ B
    RIGHT JOIN:     A ⟖ B
    FULL JOIN:      A ∪ B

✅ 实际应用建议

场景 推荐连接方式
获取匹配数据 INNER JOIN
主表必须展示,辅表可无 LEFT JOIN
辅表必须展示,主表可无 RIGHT JOIN
数据对账/差异比对/全量汇总 FULL OUTER JOIN

内连接只返回满足条件的交集数据,外连接用于保留某一边或两边的所有记录。常用的是左外连接,适合主表为主、辅表为辅的场景。外连接比内连接更全,但性能略低,因此建议根据业务场景合理选择。

自我介绍

xxx

你觉得学习我们这些技术最重要的是什么?

首先要清楚**这个技术是解决什么领域的问题**,学习技术很多方面都是用来服务业务的,结合实际业务来学习技术融合性会更强

技术栈有些不同,有没有想过换方向发展?

没问题的 因为技术是相通的 可以去学新技术

ThreadLocal相关面试题

1.概述

ThreadLocal(定义全局静态变量 项目中共用)是Java中的一个线程局部变量工具类,它提供了一种在多线程环境下,每个线程都可以独立访问自己的变量副本的机制。ThreadLocal中存储的数据对于每个线程来说都是独立的,互不干扰。

2. 使用场景

ThreadLocal适用于以下场景:

  • 在多线程环境下,需要保持线程安全性的数据访问。
  • 需要在多个方法之间共享数据,但又不希望使用传递参数的方式。
    • 在传递登录用户id是非常方便且适用

以后获取用户id不用再解析token了,线程拿仅仅拿当前线程的数据 每个登录的用户都有自己的threadlocal数据

ThreadLocal并不是一个Thread,而是Thread的局部变量【可以存储数据】
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。**ThreadLocal实现一个线程内传递数据**[就不用一个个参数往后传递了]
注意:客户端发送的每次请求,后端的tomcat服务器都会分配一个单独的线程来处理请求

  • 一个请求是一个线程[意义:在第一个线程里面使用ThreadLocal存储用户Id 在后面controller或service中就可以取出来用户id]
  • 第二个请求就是另一个线程 线程池用完第一个放回线程池 也有可能把上一个线程接着给它用

postHandle 只有在正确调用返回才会引用 如果抛出异常则不会使用
afterCompletion 无论怎样最后都要运行

ThreadLocal

3.1 创建ThreadLocal对象

首先,我们需要创建一个ThreadLocal对象来存储线程局部变量。可以使用ThreadLocal的默认构造函数创建一个新的实例。【给每个线程拷贝一份 synchn + Lock锁】

ThreadLocal<String> threadLocal = new ThreadLocal<>();
3.2 设置线程局部变量的值

使用set()方法可以设置当前线程的局部变量的值。

threadLocal.set("value");
3.3 获取线程局部变量的值

使用get()方法可以获取当前线程的局部变量的值。

String value = threadLocal.get();
3.4 清除线程局部变量的值

使用remove()方法可以清除当前线程的局部变量的值,建议在整个请求使用完一定要执行remove清除数据,不然可能会发生内存泄漏问题。

threadLocal.remove();
下面是一个简单的示例代码,演示了如何使用ThreadLocal。
public class ThreadLocalTest {

    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            THREAD_LOCAL.set("itheima");// 写入线程本地变量
            getData("t1");// 读取线程本地变量
        }, "t1");

        Thread t2 = new Thread(() -> {
            THREAD_LOCAL.set("itcast");// 写入线程本地变量
            getData("t2");// 读取线程本地变量
        }, "t2");

        t1.start();
        t2.start();

    }

    private static void getData(String threadName){
        Object data = THREAD_LOCAL.get();
        System.out.println(threadName+"-"+data);
    }
}

运行以上代码,输出结果为:

t1-itheima
t2-itcast

在任意位置都可以调用Threadlocal,线程隔离互不影响,解决了线程安全问题:[每个线程存一份 线程不共享]

用mybatis+建造者模式 一定要在类里面加 一定要具有有参和无参构造缺一不可 否则mybatis数据封装无法映射
@AllArgsConstructor
@NoArgsConstructor
@Builder

字符和字符串类型的区别

字符是基本数据类型 没有null 用单引号
字符串是引用数据类型[一个类] 用双引号

image-20250102142357092

操作字符串的工具类是什么

apache的common提供的String Utils工具类,hutool也有String Utils

字符串工具类

工具类库 类名 说明与特点
Apache Commons StringUtils (org.apache.commons.lang3.StringUtils) 提供了大量字符串处理方法,如isBlank()join()split()contains()等。非常稳定、功能强大。
Hutool StrUtil (cn.hutool.core.util.StrUtil) 轻量级工具包,功能类似,但更加面向国人开发,API 命名更简洁清晰,如 isBlank()format()hide()等。
Java 自带 String 类和 StringBuilder, StringBuffer 提供基础字符串操作,如 substring()indexOf()replace() 等。

推荐:开发中可以结合使用 Apache StringUtilsHutool StrUtil,比自己写字符串判断高效且可读性好。

1. 用户注册校验 — Apache Commons StringUtils

业务背景: 用户注册时,需要校验用户名和邮箱不能为 null 或空字符串。

import org.apache.commons.lang3.StringUtils;

public class UserService {
    public boolean isValidUser(String username, String email) {
        return StringUtils.isNotBlank(username) && StringUtils.isNotBlank(email);
    }
}

📌 优势说明:

  • StringUtils.isNotBlank() 能判断 null、空字符串、全是空格的字符串。
  • 替代了传统的 "".equals(x) || x == null || x.trim().length() == 0,简洁高效。

2. 昵称脱敏展示 — Hutool StrUtil

业务背景: 用户中心页面要展示脱敏后的昵称,如 张三丰张*丰

import cn.hutool.core.util.StrUtil;

public class UserUtil {
    public static String desensitizeName(String name) {
        return StrUtil.hide(name, 1, name.length() - 1); // 从第2位到倒数第1位隐藏
    }
}

输出示例:

System.out.println(desensitizeName("张三丰")); // 张*丰

📌 优势说明:

  • StrUtil.hide() 非常适合做数据脱敏处理(身份证、手机号等)。
  • API 直观易用,无需自己写 substring 拼接逻辑。

3. SQL拼接或日志打印 — Java 原生 StringBuilder

业务背景: 拼接一段复杂 SQL 或多字段日志输出。

public class LogBuilder {
    public static String buildLog(String userId, String action, long timestamp) {
        StringBuilder sb = new StringBuilder();
        sb.append("用户ID: ").append(userId)
          .append(", 操作: ").append(action)
          .append(", 时间: ").append(timestamp);
        return sb.toString();
    }
}

输出:

用户ID: 123456, 操作: 登录, 时间: 1721559065000

📌 优势说明:

  • StringBuilder 适合循环中或大量字符串拼接,性能比 + 更优。
  • 通常用于日志、SQL、通知信息构建等场景。

局部变量和成员变量的区别

成员变量是在jvm的
局部变量是在jvm的
基本数据类型的引用类型 类在堆
基本数据类型的成员变量在堆 非静态在堆 静态变量在方法区
引用类型 无论静态还是非静态成员变量都在堆区

局部变量是在方法或代码块内部声明的变量,其作用域仅限于声明它的方法或代码块
局部变量不能被static修饰
局部变量必须被初始化才能使用

成员变量是在类内部声明的变量,其作用域是整个类
成员变量可以被static修饰
成员变量有默认值

public class VariableDemo {

    // 成员变量
    int age = 18;              // 非静态成员变量 -> 堆中
    static String school = "北大"; // 静态成员变量 -> 方法区中

    public void printInfo() {
        // 局部变量
        String name = "小明";  // 局部变量 -> 栈中
        int age = 20;          // 局部变量,和成员变量重名(就近原则)

        System.out.println("局部变量 name:" + name); // 小明
        System.out.println("局部变量 age:" + age);   // 20
        System.out.println("成员变量 age:" + this.age); // 18(用this访问成员变量)
        System.out.println("静态变量 school:" + school); // 北大
    }

    public static void main(String[] args) {
        VariableDemo v = new VariableDemo();
        v.printInfo();
    }
}
------------------------------------------------------------------------
JVM 内存结构:

方法区(存放类信息、静态变量等)
└── VariableDemo.class
     └── static school = "北大"

堆内存(存放对象和非静态成员变量)
└── v 对象
     └── age = 18

栈内存(每个线程的栈帧)
└── main() 栈帧
     └── v 引用

└── printInfo() 栈帧
     ├── name = "小明"
     ├── age = 20 (局部变量)

你编写完代码,写完这个功能后,会进行什么操作呢

进行代码审查,检查代码是否符合编码规范和设计要求。

  • 进行单元测试,确保代码的功能正确无误。
  • 进行代码优化,提高代码的性能和可维护性。
  • 与团队成员进行代码合并,确保代码的集成。
  • 编写文档,记录功能实现和代码变更。

上一家公司的薪资是多少?期望薪资是多少?上一家还有什么其他的福利吗?

期望薪资:
了解过广东这边的市场 我想换工作想涨薪10~20%
节假日会发放礼品和福利

可以接受低代码平台吗?

可以接受
低代码平台:类若依

具体说说Java面向对象

Java面向对象是一种编程范式,它将现实世界的事物抽象成程序中的对象。Java面向对象的主要特征包括:

  • 封装:将对象的属性行为封装在一起,对外只暴露必要的接口,隐藏内部实现细节。
  • 继承:允许子类继承父类的属性和行为,实现代码的复用。
  • 多态同一个接口可以有多个不同的实现,通过对象的类型和方法的调用,实现不同的功能。

== 和 equals 的区别

  • ==:比较基本数据类型时,比较的是;比较引用数据类型时,比较的是对象的内存地址
  • equals:是Object类的一个方法,默认比较的是对象的内存地址。但在很多类中(如String、Integer等),equals方法被重写,用于比较对象的内容是否相等。
    没重写 就是 == 比较对象地址。重写过的话就比较对象的值。

有没有做过权限控制,整个系统的权限

有过 SpringSecurity

能具体说一下权限控制怎么做?

使用RBAC模型 不是把用户关联资源 而是中间利用角色间接关联
用户+角色+资源+用户角色中间表+角色资源中间表多对多

SpringSecurity 具体怎么实现

我的项目是基于JWT的前后端分离的项目,在自定义认证管理器AuthenticationManager认证成功后,生成JWT令牌并返回给前端。前端在随后的请求中携带这个JWT令牌。这时候,我们使用AccessDecisionManager来实现接口的鉴权逻辑,其中包括一个check方法,该方法会校验JWT令牌的有效性。如果校验通过,就去查询数据库以确定用户拥有哪些权限。在用户登录时,其权限信息已经被缓存到Redis中。后续的请求中,我们可以直接从Redis中检索用户的权限信息。如果请求的接口权限与用户缓存中的权限匹配,则放行;如果不匹配,则返回一个友好的错误信息。

有没有做过权限控制?

有,在项目中我用的是 Spring Security + JWT 的权限控制方案,整体基于 RBAC 模型(基于角色的访问控制)。

权限模型设计(RBAC)

核心思路:通过“角色”来间接管理用户权限

实体 关系说明
用户(User) 关联多个角色(Role)
角色(Role) 关联多个权限/菜单(Permission)
权限(Permission) 描述资源:菜单、接口等

中间表设计:

  • 用户角色表(user_role)→ 多对多
  • 角色权限表(role_permission)→ 多对多

SpringSecurity 是如何实现的?

项目是 前后端分离,使用的是 JWT 无状态认证

1️⃣ 用户登录认证(AuthenticationManager

  • 用户登录提交账号密码 → 调用 SpringSecurity 的认证逻辑
  • 认证通过后,生成 JWT Token 返回给前端前端在随后的请求中携带这个JWT令牌
  • 同时把 用户信息 + 权限信息 缓存在 Redis 中

2️⃣ 用户请求鉴权(拦截 + 校验)

用户每次访问接口都带上 JWT:使用AccessDecisionManager来实现接口的鉴权逻辑,其中包括一个check方法,该方法会校验JWT令牌的有效性

核心流程:

请求到来 → JWT过滤器提取Token → 解析用户信息 → 加载权限 → 比对是否有权限 → 放行 or 拦截

具体细节:

  • 使用 自定义过滤器 替换 UsernamePasswordAuthenticationFilter
  • Token 中取出用户名 → 去 Redis 中拿权限信息(避免每次都查库)
  • 使用 AccessDecisionManagerSecurityMetadataSource 判断当前接口所需权限是否被用户拥有
  • 校验成功:放行;校验失败:返回 403 自定义响应(如“无权限访问”)

为什么使用 Redis 缓存权限?

  • 避免每次请求都去查数据库,提升性能
  • 实现单点登录、Token失效管理等操作更方便
  • 权限信息改动 → Redis 也同步更新

如何定义哪些接口需要权限?

  • 使用 @PreAuthorize("hasAuthority('xxx')")
  • 或者用自定义权限注解 + AOP 实现

✅ 总结一句话回答

我使用的是基于 JWT + SpringSecurity 的权限控制,采用的是 RBAC 模型,通过用户角色权限的绑定来控制资源访问。权限信息登录时就缓存到 Redis,提高了性能,接口鉴权用的是自定义的 AccessDecisionManager 来校验 Token 与权限是否匹配,确保接口安全。

✅ 场景描述:权限精确到按钮级别

不仅控制用户能否访问“某个菜单”,还要控制:

  • 某些用户能看到“编辑”按钮
  • 某些用户不能看到“删除”按钮
  • 某些按钮点击后后台要鉴权(如“导出”、“审核”等)

🧩 一、数据库设计(基于 RBAC 模型)

1️⃣ 表结构设计(核心五张表)

表名 说明
user 用户表
role 角色表
permission 权限资源表(菜单、接口、按钮)
user_role 用户与角色的多对多关联
role_permission 角色与权限的多对多关联

2️⃣ permission 权限表示例(关键)

字段 说明
id 主键
name 权限名称,例如“删除按钮”
type 类型:menubuttonapi
perm_code 权限标识,例如:user:deleteuser:edit
parent_id 上级权限(菜单树结构)
url / path 前端路由或接口路径
method 请求方式:GET/POST/DELETE(接口专用)

🚨 重点:按钮权限是存放在 permission 表里的,只是 type = button,和菜单/接口共用一个表。

🔁 二、后端逻辑处理流程

1️⃣ 用户登录时加载权限

@Override
public UserDetails loadUserByUsername(String username) {
    // 1. 查询用户
    User user = userMapper.selectByUsername(username);

    // 2. 查询权限(按钮 + 接口 + 菜单)
    List<String> permissionCodes = permissionMapper.getUserPerms(user.getId());

    // 3. 存入 UserDetails
    return new LoginUser(user, permissionCodes);
}

✔️ 这些 permissionCodes 会通过 JWT 或 Redis 存起来,后续用来鉴权和前端展示。

2️⃣ 鉴权控制(精确到按钮)

你需要:

  • 后端校验接口权限(如调用删除接口必须拥有 user:delete 权限)
  • 前端展示按钮要判断是否有权限

🧷 方法一:接口权限控制(后端)

@PreAuthorize("hasAuthority('user:delete')") // ★★★★
@DeleteMapping("/user/delete/{id}")
public void deleteUser(@PathVariable Long id) {
    // 业务逻辑
}

✔️ 只有拥有 user:delete 权限的用户才能调用这个接口。


🧷 方法二:按钮权限控制(前端)

<!-- Vue 示例 -->
<template>
  <div>
    <el-button v-if="hasPermission('user:edit')">编辑</el-button>
    <el-button v-if="hasPermission('user:delete')">删除</el-button>
  </div>
</template>
// 权限判断函数
export function hasPermission(code) {
  const perms = JSON.parse(localStorage.getItem("userPerms")) || [];
  return perms.includes(code);
}
🛠 三、JWT + Redis 权限缓存方案(提高性能)
登录成功后:
// 登录成功后:
String token = jwtUtil.generateToken(userId);

// Redis缓存用户权限
redis.set("login:" + userId, JSON.stringify(LoginUser对象)) // 包括 permissionCodes

// 返回token给前端
鉴权过滤器:
// Token过滤器从请求头获取token → 从Redis读取用户权限 → 设置SecurityContext

📐 四、权限结构如何传给前端?

示例:

[
  {
    "menuName": "用户管理",
    "path": "/user",
    "children": [
      {
        "menuName": "用户列表",
        "path": "/user/list",
        "permissions": ["user:add", "user:delete", "user:edit"]
      }
    ]
  }
]

前端根据这些 permissions 控制按钮显示。


✅ 总结一句话回答:

在项目中我们实现了精确到按钮级别的权限控制。使用 RBAC 模型统一管理“菜单、接口、按钮权限”,后端统一校验权限(用 Spring Security 的 @PreAuthorize),前端根据用户权限码动态控制按钮的显示,用户权限在登录时就加载并缓存到 Redis + JWT 中,实现了性能与安全的平衡。

✅ 真实项目中常见补充点(面试必问)

问题 解答
前端怎么知道自己有哪些权限? 登录后从后端返回权限码数组,如 ["user:add", "user:delete"],存在 Vuex / Pinia 或 localStorage 中
权限更新了怎么办? 更新用户角色后,同时刷新 Redis 缓存或设置 token 过期时间较短
一个按钮需要多个权限怎么办? v-if="hasPermission('user:add') && hasPermission('user:export')"

我想要你详细讲一下 数据库的那张权限表 帮我模拟一些字段和数据 实现达到按钮级别

我们就来详细 模拟一张完整的权限表(permission,并:

  1. 定义它的字段含义
  2. 填入一些真实的示例数据(精确到按钮级别
  3. 分析如何建立菜单树、按钮权限和接口权限之间的关系
  4. 说明这些数据在前后端中如何使用

🧩 一、权限表(permission)字段设计

字段名 类型 示例值 含义说明
id BIGINT 1001 权限唯一标识(长整型,通常是雪花ID)
name VARCHAR "用户管理" 权限名称(菜单/按钮/接口名称)
type VARCHAR "menu" / "button" / "api" 权限类型,支持菜单、按钮、接口
parent_id BIGINT 0 / 1001 / 1002 上级ID(0 表示顶级)
perm_code VARCHAR "user:list" / "user:delete" 权限标识,用于校验
url / path VARCHAR "/user/list" / "/api/user/delete" 前端路由或后台接口地址
method VARCHAR "GET" / "POST" / "DELETE" 请求方式(用于接口)
sort INT 1 / 2 / 3 排序字段
status TINYINT 1 启用状态:1启用、0禁用
visible TINYINT 1 是否前端可见(按钮用)

📌 二、模拟真实权限表数据(示例)

假设我们有一个系统模块叫“用户管理”,有一个菜单和三个按钮:

  • 用户管理(菜单)
  • 查询用户(按钮)
  • 编辑用户(按钮)
  • 删除用户(按钮)

我们再加上一个接口权限作为后台控制。【这里idparent_id很重要】

id name type parent_id perm_code path method visible sort
1000 用户管理 menu 0 user:root /user null 1 1
1001 用户列表 menu 1000 user:list /user/list null 1 1
1002 查询按钮 button 1001 user:query null null 1 1
1003 编辑按钮 button 1001 user:edit null null 1 2
1004 删除按钮 button 1001 user:delete null null 1 3
1005 删除用户API api 1001 user:delete /api/user/delete DELETE 0 4

🧠 三、说明和作用分析

1️⃣ type = menu

  • 表示是页面菜单节点
  • 前端构建菜单树的依据
  • path字段,用于前端路由匹配

2️⃣ type = button

  • 表示按钮级别权限
  • 前端判断按钮显隐时使用:
hasPermission('user:delete') ? 显示删除按钮 : 不显示

3️⃣ type = api

  • 表示后台接口权限
  • 由 Spring Security + @PreAuthorize("hasAuthority('user:delete')") 控制接口是否放行

4️⃣ perm_code

  • 每一个权限都有一个唯一 perm_code,即权限码
  • 按钮和API**可以共用一个 perm_code**,代表前后端权限打通

🧾 附:完整表结构建表SQL

CREATE TABLE `permission` (
  `id` BIGINT PRIMARY KEY,
  `name` VARCHAR(100) NOT NULL,
  `type` VARCHAR(20) NOT NULL COMMENT 'menu, button, api',
  `parent_id` BIGINT DEFAULT 0,
  `perm_code` VARCHAR(100),
  `path` VARCHAR(200),
  `method` VARCHAR(10),
  `visible` TINYINT DEFAULT 1,
  `sort` INT DEFAULT 1,
  `status` TINYINT DEFAULT 1
);

线程池有哪些状态,这些状态是怎么进行转换的

线程池有以下几种状态:

  • RUNNING:线程池正常运行,可以接受新的任务和处理任务队列中的任务。
  • SHUTDOWN:线程池不再接受新的任务,但会处理任务队列中的任务。
  • STOP:线程池不再接受新的任务,也不处理任务队列中的任务,并且会中断正在执行的任务。
  • TIDYING:所有任务都已终止,线程池即将关闭。
  • TERMINATED:线程池已关闭。

状态转换过程如下:

  • RUNNING -> SHUTDOWN:调用shutdown()方法。
  • RUNNING -> STOP:调用shutdownNow()方法。
  • SHUTDOWN -> TIDYING:当线程池和任务队列都为空时。
  • STOP -> TIDYING:当线程池为空时。
  • TIDYING -> TERMINATED:当terminated()钩子方法执行完成后。

说一下怎么使用多线程?

  • 继承Thread类,并重写run()方法。

  • 实现Runnable接口,并将实现类传递给Thread对象。【无返回值】

  • 实现Callable接口,实现call()方法【有返回值】

  • 使用Executor框架,如ExecutorService和ThreadPoolExecutor来管理线程池。

操作系统上的线程有多少种状态[5]?Java线程有多少种状态[6]?

  • 新建(New):创建后尚未启动的线程处于这个状态。new Thread
  • 可运行(Runnable):包括运行(Running)和就绪(Ready)状态,线程正在执行或等待CPU调度。
  • 阻塞(Blocked):线程因为等待某些资源或锁而被阻塞。notify可以唤醒阻塞状态 睡眠完会自动唤醒
  • 等待(Waiting):线程等待其他线程执行特定操作(如通知)。
  • 计时等待(Timed Waiting):线程在一定时间内等待另一个线程的通知。
  • 终止(Terminated):线程执行完成或因异常而终止。
怎么把线程杀死 终止

stop()方法[暴力方法] interrupt()方法[优雅关闭线程] 正常回收

乐观锁和悲观锁的区别

乐观锁:读多写少 线程执行时间相差较大 并发不太激烈

悲观锁:写多读少 线程执行时间相差不大 竞争激烈 并发锁多

加锁的时机不一样,
悲观锁:没改数据的时候先加锁 比较明显利用底层操作系统api实现
乐观锁:在改数据的时候才加锁 依靠底层的硬件

java层面synchronized ReentrantLock

数据库层面
悲观锁:select for update是mysql的的实现
乐观锁:JUC Java Util Concurrent)是Java并发工具包

SELECT ... FOR UPDATE:这个语句在读取记录时会锁定这些记录,直到事务提交或回滚。其他的事务不能更新这些锁定的记录,这是悲观锁的一个典型实现

乐观锁要读取目前旧的值再将新设置的值以及旧的值比较 如果相同 就把新的值更新 如果不相同 就把旧的值重新提取 因为在这期间有人读取了这个数据跟我之前不一样(底层api 要调用两个 一个旧的值 一个新的值)。一般乐观锁是结合自旋 类于while(true)直到读到为止 要设计次数后再报错

要更新数据库某个值 把旧的值读出来 想更新银行里的余额
这是典型的ABA问题要用时间戳自增版本号去做

🔒 各种锁对比表格

锁类型 适用场景 优点 缺点
乐观锁(version) 冲突少、性能优先 无阻塞、效率高 有冲突时需重试,自旋成本高
悲观锁(for update) 冲突多、安全性要求高 强一致性 并发性能差,可能导致死锁
分布式锁(Redis/Redisson) 多服务/微服务场景 支持跨节点资源锁定 实现复杂,依赖中间件
本地锁(synchronized/Lock) 单服务场景 实现简单 无法跨JVM,分布式无效

Stream流的使用及常用API

Stream是Java 8中引入的一种新特性,用于简化数据处理和操作。它可以用来解决集合循环遍历处理的问题。在此之前用循环来代替

基础Stream操作

  • stream(): 为集合创建串行流。
  • parallelStream(): 为集合创建并行流。
  • forEach: 对每个元素执行操作。
  • map: 将每个元素映射到对应的结果。
  • filter: 过滤出满足条件的元素。
  • limit: 限制流的大小。
  • skip: 跳过流中的前n个元素。
  • sorted: 对流进行排序。

终端操作

  • collect: 将流转换为其他形式,比如列表、集合或Map。
  • reduce: 通过一个起始值,反复利用BinaryOperator来处理和累积元素,返回一个值。
  • count: 返回流中元素的数量。
  • min / max: 找到流中的最小/最大值。
  • anyMatch: 流中是否有一个元素匹配给定的谓词。
  • allMatch: 流中的所有元素是否都匹配给定的谓词。
  • noneMatch: 流中没有任何元素匹配给定的谓词。
  • findFirst: 返回第一个元素。
  • findAny: 返回当前流中的任意元素。

🧩 场景 1:筛选出余额大于1000的用户列表(filter

List<User> richUsers = userList.stream()
    .filter(user -> user.getBalance().compareTo(BigDecimal.valueOf(1000)) > 0)
    .collect(Collectors.toList());

重点理解: filter 用来“过滤”,留下满足条件的用户。


🧩 场景 2:获取所有用户名组成的新列表(map

List<String> usernameList = userList.stream()
    .map(User::getUsername)
    .collect(Collectors.toList());

重点理解: map 是映射,通常是将对象转换成某个字段或新结构。


🧩 场景 3:对订单按金额排序(sorted

List<Order> sortedOrders = orderList.stream()
    .sorted(Comparator.comparing(Order::getAmount))
    .collect(Collectors.toList());

重点理解: sorted 用于排序,自定义排序字段。


🧩 场景 4:统计多少个订单金额大于1000(filter + count

long highValueOrderCount = orderList.stream()
    .filter(order -> order.getAmount().compareTo(BigDecimal.valueOf(1000)) > 0)
    .count();

重点理解: count() 是终止操作,直接返回数量。


🧩 场景 5:判断是否有用户未激活(anyMatch

boolean hasInactive = userList.stream()
    .anyMatch(user -> !user.isActive());

重点理解: anyMatch 检查是否存在一个满足条件的元素。


🧩 场景 6:求所有订单的总金额(map + reduce

BigDecimal totalAmount = orderList.stream()
    .map(Order::getAmount)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

重点理解: reduce 是归约,把多个值合成一个值。


🧩 场景 7:跳过前5条,取后5条(skip + limit

List<Order> lastFive = orderList.stream()
    .skip(orderList.size() - 5)
    .limit(5)
    .collect(Collectors.toList());

重点理解: 用于分页、滚动加载的典型场景。


🧩 场景 8:分组统计——按用户ID分组他们的订单(collect(Collectors.groupingBy)

Map<Long, List<Order>> ordersByUser = orderList.stream()
    .collect(Collectors.groupingBy(Order::getUserId));

重点理解: groupingBy 是分组聚合操作。


🧩 场景 9:提取用户ID组成 Set 去重(map + collect(Collectors.toSet())

Set<Long> userIds = userList.stream()
    .map(User::getId)
    .collect(Collectors.toSet());

重点理解: 利用 Set 实现自动去重。


🧩 场景 10:查找第一个未发货订单(filter + findFirst

Optional<Order> firstUnshipped = orderList.stream()
    .filter(order -> !order.isShipped())
    .findFirst();

重点理解: findFirst() 返回的是 Optional<T>,避免空指针。


✅ 总结表:常用 Stream 操作一图读懂

操作方法 功能说明 示例
filter() 过滤数据 filter(u -> u.age > 18)
map() 映射转换 map(User::getId)
sorted() 排序 sorted(Comparator.comparing())
limit() 限制返回个数 limit(10)
skip() 跳过前面几个 skip(5)
collect() 收集结果 collect(Collectors.toList())
count() 计数 .count()
anyMatch() 是否有任意一个满足 anyMatch(u -> !u.active)
allMatch() 所有都满足 allMatch(u -> u.age > 0)
reduce() 累积求和等 reduce(0, Integer::sum)
findFirst() 返回第一个 findFirst()

总结一下lambdaquerywrapper 和 querywrapper的区别 还有他们各自的真实场景

✅ 一句话区分:

类型 特点 使用方式
QueryWrapper 通过 字段名字符串 构造条件 .eq("username", "Tom")
LambdaQueryWrapper 通过 方法引用(安全) 构造条件 .eq(User::getUsername, "Tom")

✳️ 1. QueryWrapper:字段名写字符串的方式

✅ 特点:

  • 字段名用字符串传入,如 "username"
  • 使用简单、灵活、直观。
  • 但容易拼写错误,且不支持字段重构(字段名变了不会报错)

✅ 示例:

QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", "Tom")
       .ge("age", 18)
       .like("email", "@gmail.com");

List<User> list = userMapper.selectList(wrapper);

✅ 适用场景:

  • 快速写 Demo、测试。
  • 字段是动态传入的(如:前端传字段名)——需要用字符串拼字段名。
  • 表字段不会频繁变动。

✳️ 2. LambdaQueryWrapper:方法引用式,更安全!

✅ 特点:

  • 使用 方法引用(如 User::getUsername),可以避免拼写错误。
  • 编译时检查字段名,重命名字段 IDE 可自动修改引用。
  • 更加 类型安全、可维护

✅ 示例:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, "Tom")
       .ge(User::getAge, 18)
       .like(User::getEmail, "@gmail.com");

List<User> list = userMapper.selectList(wrapper);

✅ 适用场景:

  • 实际项目开发中推荐优先使用。
  • 字段会变,怕忘改地方。
  • 追求代码规范、可读性和安全性。

🔍 场景对比总结:

需求/场景 推荐使用 原因
字段名是前端传过来的字符串 QueryWrapper 字段名只能动态拼接
普通查询(写死字段名) LambdaQueryWrapper 编译器校验字段名,防止拼写错误
想快速测试或写 Demo QueryWrapper 简洁方便
表结构变动频繁、重构频繁 LambdaQueryWrapper 字段引用会被 IDE 识别

LambdaQueryWrapper真实场景

✅ 场景一:精确查询:查某个用户是否存在

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, "admin");

User user = userMapper.selectOne(wrapper);

说明:判断用户名为 admin 的用户是否存在。eq 表示等于。


✅ 场景二:范围查询:查年龄在 18~30 岁之间的用户

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.between(User::getAge, 18, 30);

List<User> users = userMapper.selectList(wrapper);

说明:between 等价于 age >= 18 AND age <= 30


✅ 场景三:模糊查询:查邮箱包含“@gmail”的用户

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getEmail, "@gmail");

List<User> users = userMapper.selectList(wrapper);
like` 相当于 SQL 中的 `LIKE '%xxx%'

✅ 场景四:多条件查询:查 username 为 tom,且状态为启用

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, "tom")
       .eq(User::getStatus, 1); // 1 代表启用

List<User> users = userMapper.selectList(wrapper);

✅ 场景五:动态查询:某些参数可为空,不传就不作为查询条件

String username = "tom";
Integer age = null;

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(username), User::getUsername, username)
       .eq(age != null, User::getAge, age);

List<User> users = userMapper.selectList(wrapper);

⭐ 动态条件拼接是 MyBatis-Plus 的一大特色:条件为 true 才拼入 SQL


✅ 场景六:排序查询:按注册时间倒序

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(User::getCreateTime);

List<User> users = userMapper.selectList(wrapper);

✅ 场景七:分页查询 + 条件筛选

Page<User> page = new Page<>(1, 10); // 第1页,每页10条

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getUsername, "张")
       .ge(User::getAge, 18);

IPage<User> userPage = userMapper.selectPage(page, wrapper);

selectPage 返回分页对象 IPage,包含总条数、总页数、当前页数据。


✅ 场景八:统计某状态下的用户数量

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 1);

Long count = userMapper.selectCount(wrapper);

✅ 场景九:只查指定列:查所有用户名和邮箱

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.select(User::getUsername, User::getEmail);

List<User> users = userMapper.selectList(wrapper);

✅ 场景十:删除某个年龄段的用户(慎用!)

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.lt(User::getAge, 10); // 年龄小于10岁

userMapper.delete(wrapper);

🧠 总结你可以记住几个关键词:

方法 功能
eq / ne 等于 / 不等于
like / notLike 模糊匹配
gt / ge 大于 / 大于等于
lt / le 小于 / 小于等于
between 范围查询
in / notIn 集合查询
isNull / isNotNull 判空
orderByAsc / orderByDesc 排序
select 指定返回字段
last 拼接SQL片段,比如分页的 limit

挑战高级复杂场景

🧩 场景设定:后台用户综合筛选

你是某后台系统开发人员,领导提出了一个用户综合查询需求:

“我要你查出所有状态正常的用户(status=1),要求他们的:

  • 昵称中包含”超”
  • 年龄在 20 到 30 岁之间
  • 注册时间要大于某个时间点(比如 2023-01-01)
  • 邮箱不能为 null
  • 并且用户类型不能是 admin
  • 用户ID必须在某一批导入的 id 集合中
  • 最后按照注册时间降序、年龄升序排序
  • 返回分页数据,每页10条,只查 id, nickname, email, createTime 字段。”
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();

wrapper
    .eq(User::getStatus, 1)                         // 状态正常
    .like(User::getNickname, "超")                  // 昵称模糊匹配
    .between(User::getAge, 20, 30)                  // 年龄在20~30之间
    .gt(User::getCreateTime, LocalDate.of(2023, 1, 1)) // 注册时间大于2023-01-01
    .isNotNull(User::getEmail)                     // 邮箱不为null
    .ne(User::getUserType, "admin")                // 类型不能是admin
    .in(User::getId, idList)                       // id必须在给定集合中
    .orderByDesc(User::getCreateTime)              // 注册时间降序
    .orderByAsc(User::getAge)                      // 年龄升序
    .select(User::getId, User::getNickname, User::getEmail, User::getCreateTime); // 只查这几个字段

✅ 配合分页使用:

Page<User> page = new Page<>(1, 10); // 第1页,每页10条
IPage<User> result = userMapper.selectPage(page, wrapper);

🧩 变种考点:动态条件拼接

比如昵称可能为空,idList可能为空,我们加上动态判断:

wrapper
    .eq(User::getStatus, 1)
    .like(StringUtils.isNotBlank(nickname), User::getNickname, nickname)
    .between(minAge != null && maxAge != null, User::getAge, minAge, maxAge)
    .in(CollectionUtil.isNotEmpty(idList), User::getId, idList);

LambdaQueryWrapper 是最适合在真实业务中构建动态、复杂、类型安全 SQL 的利器,配合 Stream、分页、动态参数、条件拼接,几乎能满足所有后台 CRUD 需求。

项目中具体用到哪些设计模式

单例模式:确保一个类只有一个实例,例如配置文件管理器。[Spring原本设计好的]
工厂模式:创建对象时无需指定具体的类,例如日志工厂。
观察者模式:当一个对象状态发生改变时,
所有依赖于它的对象
都得到通知并自动更新,例如事件监听。
**策略模式**:定义一系列算法,将每个算法封装起来,并使它们可以互换,例如支付策略。
模板方法模式:在项目中,我有一些具有相同操作步骤但具体实现不同的算法,我使用了模板方法模式来定义这些步骤的骨架,将具体的步骤实现留给子类。任链模式的目的是将请求的发送者和接收者解耦,从而使得多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
责任链模式:它允许将请求沿着处理者链进行发送。收到请求后,每个处理者都有机会对请求进行处理,或者将其传递给链上的下一个处理者。这样,请求就能在一系列处理者中传递,直到有一个处理者对其进行处理为止。

1.递归方式 :在递归模式中,每个处理者内部调用下一个处理者的处理方法。如果当前处理者无法处理请求,它会直接调用下一个处理者的处理方法。这种方式通常是通过递归调用来实现的
2.迭代模式:在迭代模式中,处理者链被构建为一个线性结构,每个处理者都有一个指向下一个处理者的引用。请求从链的第一个处理者开始,依次传递给下一个处理者,直到找到能够处理该请求的处理者为止。这种方式通常是通过循环迭代来实现的
代理模式:为了控制对远程服务的访问,我使用了代理模式。代理负责处理所有与服务对象的交互,并在必要时进行延迟加载。

在我的项目中结合工厂模式策略模式来设计登录接口时,我们可以将登录验证的逻辑抽象为一个策略接口,并为每种登录方式(如:用户名密码登录、手机验证码登录、社交账号登录等)实现具体的策略类。工厂类则负责创建并管理这些策略对象

🧠 一句话理解每个模式(先记关键词)

模式名 核心关键词 常用场景
单例模式 只创建一次,全局唯一 Spring 中的 Bean、配置类
工厂模式 创建对象不关心具体类 日志、支付、解析器、登录类型等
策略模式 多种行为自由切换,面向接口 登录方式、支付方式、算法切换
模板方法模式 公共流程固定,步骤个别实现 钩子函数、抽象类调用子类流程
责任链模式 多个处理器按顺序处理请求 参数校验链、日志链、权限校验链
观察者模式 一变多变,通知机制 事件总线、消息订阅、MQ监听
代理模式 中间人控制访问/增强 RPC远程调用、权限控制、缓存等

📦 实战分类 + 项目应用

1️⃣ 行为型模式

✅ 策略模式(Strategy)【最常考🔥】
  • 特点:行为可切换
  • 场景:多个登录方式(手机号 / 密码 / 第三方登录)
  • 怎么用
    • 定义统一接口 LoginStrategy
    • 编写不同策略类 PhoneLoginStrategyPwdLoginStrategy
    • 用工厂或Map封装,接收一个登录类型参数调用 .doLogin()
✅ 责任链模式(Chain of Responsibility)
  • 特点:多个处理器一个接一个处理
  • 场景:表单参数校验、下单流程、审批流程
  • 递归 or 迭代实现
    • 迭代:List + for循环
    • 递归:一个 Handler 内部 next.handle(request)
✅ 模板方法模式(Template Method)
  • 特点:父类定义算法框架,子类实现步骤
  • 场景:支付流程、通知发送(短信/微信)
  • 怎么用
    • 抽象类定义 checkParamsbuildRequestsend
    • 子类实现这些步骤
✅ 观察者模式(Observer)
  • 特点:一个变化通知多个订阅者
  • 场景:MQ消费后触发多个事件、发布文章后通知关注者
  • 怎么用
    • Spring 的 @EventListener
    • Guava EventBus / Disruptor

2️⃣ 创建型模式

✅ 工厂模式(Factory)
  • 特点:创建对象解耦
  • 场景:创建不同类型的策略类、构建不同报文体
  • 怎么用
    • 定义工厂类,根据参数 new 不同类或从 map 中 get
✅ 单例模式(Singleton)
  • 特点:只创建一个对象,节省资源
  • 场景:配置类、工具类、缓存类、Spring 默认就是单例

3️⃣ 结构型模式

✅ 代理模式(Proxy)
  • 特点:给目标对象加一层控制
  • 场景:权限控制、RPC远程调用、限流、缓存
  • 怎么用
    • JDK动态代理、CGLIB
    • Spring AOP就是代理模式(加日志、权限等)
行为有策略,流程靠责任;骨架定模板,通知用观察;对象建工厂,中间靠代理,唯一单实例。
思考一个问题:哪些方式创建单例模式?

1. 懒汉式,线程不安全

这种方式在类加载时不初始化。在需要的时候才创建对象,节约资源。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2. 懒汉式,线程安全

通过同步方法确保线程安全。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3. 饿汉式

类加载时就完成了初始化,保证了线程的安全性。容易浪费资源

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

4. 双重校验锁

线程安全且在实例域需要延迟加载时提高性能。

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

5. 静态内部类

这种方式既实现了懒加载,又保证了线性安全。

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

6. 枚举

实现单例的最佳方法,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象。

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

MySQL支持四种隔离级别

  • 第一个读未提交(readuncomm itted)它解决不了刚才提出的所有问题,一般项目中也不用这个。存在脏读问题 可解决不可重复读 幻读
    **不可解决脏读**:读未提交允许一个事务读取另一个事务尚未提交的数据变更。如果一个事务读取了另一个事务的未提交数据,然后那个事务回滚,则第一个事务读取的数据就是无效的(脏数据)

  • 第二个读已提交(readcomm ited)它能解决脏读的问题的,但是解决不了不可重复读和幻读。
    **解决脏独**:读已提交确保一个事务只能读取已经提交的数据变更。如果一个事务正在修改某些数据,那么在它提交之前,其他事务不能读取这些数据。这样就可以避免脏读。
    **不可解决不可重复读**:一个事务在读取某些数据后,另一个事务修改了这些数据并提交,导致第一个事务再次读取时得到不同的结果
    **不可解决幻读**:一个事务在读取某个范围的数据后,另一个事务插入了一些新数据并提交,导致第一个事务在执行范围查询时看到了新插入的数据

  • 第三个可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读[解决了一部分],这个也是mysql默认的隔离级别。

  • 第四个串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。
    串行化里的读也要加锁 表锁:整个表上锁 行锁:只对一行加锁
    串行化是最严格的事务隔离级别。它通过强制事务串行执行来避免上述所有问题。在一个事务执行时,它会锁定涉及的所有数据行或表,直到事务完成。这确保了事务完全隔离,但是会显著降低系统的并发性能

什么时候上行锁/表锁?

INSERT不带查询筛选条件 上行锁底层是索引,b+树底层叶子

  • 行锁:通常情况下,插入操作会锁定插入行所在的索引项,以防止其他事务同时修改同一行。这是因为数据库通常使用B+树来维护索引,插入操作需要在B+树中找到正确的位置来插入新的索引项。如果插入操作涉及到唯一索引,数据库还会检查是否有重复的键值,这也会触发行锁。
  • 注意:即使插入操作没有查询筛选条件,它仍然可能涉及到行锁,因为数据库需要保证新插入的数据不会与现有数据冲突。

UPDATE看where后面的条件 带索引加行锁构建b+树 不带索引的加表锁
表锁的速度比行锁速度快

带索引的条件:
  • 行锁:如果更新操作的条件是索引列,数据库能够快速定位到需要更新的行,因此只会锁定那些特定的行。行锁可以最大程度地减少锁定的数据量,从而提高并发性能。
  • 原理:数据库使用B+树索引来快速查找满足条件的行,然后对这些行加锁。
不带索引的条件:
  • 表锁:如果更新操作的条件不是索引列,数据库可能需要扫描整个表来找到需要更新的行。在这种情况下,为了简化锁定逻辑并防止在扫描过程中数据被修改,数据库可能会选择锁定整个表。
  • 原理:由于没有索引可以利用,数据库必须检查每一行来确定是否满足更新条件,因此使用表锁可以避免复杂的锁定管理。

MVCC底层是多版本并发控制 但底层并不怎么了解

🧩 二、锁的判断依据

操作类型 是否使用索引? 锁类型 说明
INSERT 和是否冲突有关 行锁或表锁 INSERT默认加意向排他锁 + 行锁,唯一索引冲突时加锁更复杂
UPDATE WHERE使用了索引 行锁(推荐) 命中索引范围 → 行锁;可并发更新不同数据行
UPDATE WHERE没用索引 表锁(不推荐) 扫描全表找目标 → 表锁;阻塞性大
DELETE 和 UPDATE 类似 同上 删除符合条件的行,和更新逻辑一样
SELECT 普通查询 无锁(MVCC) 读不加锁,靠快照版本
SELECT ... for update 使用索引? 行锁或表锁 强制加锁,依赖索引判断行锁/表锁

ABA 问题(乐观锁延申)

你前面说得对,在并发高场景中,即使数据值一样也不能认为没变,这就是ABA问题

  • A线程读了版本为1的数据,准备更新
  • B线程将数据从A改为B,又改回A,版本号变了
  • A线程再更新时发现版本不一致,避免了误操作!

解决方式就是:

  • 使用 version 字段 + 乐观锁更新 WHERE version = ?
  • 如果更新失败,重试或提示失败(可加重试次数)

在MySQL中是否加行锁还是表锁,取决于是否命中了索引。

  • 对于 UPDATE / DELETE 等 DML 语句,如果 WHERE 条件命中索引,InnoDB 会加行锁
  • 如果未命中索引,则会加表锁,以保证操作的原子性和一致性。
    插入 INSERT 一般是对 B+ 树页加意向锁和插入位置加锁,如果涉及唯一索引校验也会加锁。
    对于并发控制,MySQL采用MVCC机制来实现非阻塞读写,在读时不加锁而是使用快照版本。
    如果业务涉及乐观锁防止并发更新错误,还可以通过版本号控制来避免ABA问题。

深拷贝和浅拷贝的区别?

浅拷贝:只复制对象的基本数据类型引用类型地址,不复制引用类型指向的对象。如果原对象和浅拷贝对象中的一个改变了引用类型,另一个也会受到影响。旧对象改变新对象也会改变。
深拷贝:创建一个新的对象,并复制对象的所有字段,包括基本数据类型和引用类型指向的对象。原对象和深拷贝对象之间不会相互影响。旧对象改变新对象不会改变
Java是值传递

如何实现深拷贝?数组不需要重写【体现了原型设计模式
  • 实现Cloneable接口并重写clone方法 会调用构造方法

    这是最常见的实现深拷贝的方法。首先,你的类需要实现Cloneable接口,然后重写clone()方法构造新对象的过程,并在该方法中调用super.clone(),同时递归地克隆所有引用类型的字段。[如果里面有多层嵌套复杂对象 在每层都要实现Cloneable接口一直重写到基本数据类型的时候才停止]

public class Person implements Cloneable {
    private int age;
    private Address address;

    // 构造器、getter、setter 省略

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        // 假设Address也实现了Cloneable接口
        cloned.address = (Address) this.address.clone(); 
        return cloned;
    }
}

public class Address implements Cloneable {
    private String street;
    private String city;

    // 构造器、getter、setter 省略

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
  • 通过序列化对象转二进制反序列化二进制转对象。这种方式不需要实现Cloneable接口,但你的类需要实现Serializable接口。反序列化不会调用构造方法
维度 浅拷贝 深拷贝
基本数据类型 复制值 复制值
引用类型 复制引用地址,指向同一对象 复制引用所指向的对象开辟新内存空间
是否影响原对象 会影响(共享引用) 不会影响(各自独立)
性能 快,开销小 慢,递归复制所有对象
使用场景 数据不变或只读场景 数据完全隔离、需要独立副本场景

在Java中,浅拷贝仅复制对象的基本类型值和引用类型地址,因此多个对象可能共享同一引用,一方修改会影响另一方;而深拷贝则会递归复制所有引用对象,完全隔离副本。

通常浅拷贝使用 super.clone() 即可,而深拷贝需要实现 Cloneable 接口 + 手动对每一个引用类型递归克隆,或使用序列化方案更为简洁。深拷贝体现了原型设计模式的思想,适合于需要保留对象快照、撤销功能、缓存隔离等场景。

开启线程的时候为什么用的是thread.start方法:

thread.start()方法用于启动一个新线程,并执行该线程的run()方法。调用start()方法后,线程会被放入线程调度队列,等待CPU调度执行。

直接调用run()方法,并不会启动一个新线程,而是在当前线程中执行run()方法,这不符合多线程编程的目的。使用start()方法可以确保线程并发执行,提高程序的性能和响应速度。

java没权限开启一个线程 要调用底层的操作系统 在JVM的底层实现中,会有相应的本地(C或C++)方法来处理线程的创建和管理

你在你们项目中使用过多线程吗?

是的,在我们的项目中,我确实使用过多线程。 【结合项目去说】
在处理大量数据计算或执行耗时的IO操作时,我会使用Java的线程池(如ExecutorService)来并行处理任务,以提高系统的响应速度和吞吐量
image-20250107153246645

我们将使用多线程来处理一个在线电子商务平台的后台订单处理系统
项目需求

该系统需要处理大量的订单,包括订单验证、库存检查、支付处理和订单状态更新。为了提高处理效率,我们决定使用多线程来并行处理订单。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.Random;

public class OrderProcessingSystem {

    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        int numberOfProcessors = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(numberOfProcessors);

        // 模拟订单队列
        Random random = new Random();
        for (int orderId = 1; orderId <= 100; orderId++) {
            int finalOrderId = orderId;
            executor.submit(() -> {
                processOrder(finalOrderId, random.nextInt(1000));
            });
        }

        // 关闭线程池
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    private static void processOrder(int orderId, int orderAmount) {
        System.out.println("Processing order ID: " + orderId + " - Thread: " + Thread.currentThread().getName());
        
        // 模拟订单验证
        validateOrder(orderId);
        
        // 模拟库存检查
        checkInventory(orderId);
        
        // 模拟支付处理
        processPayment(orderId, orderAmount);
        
        // 更新订单状态
        updateOrderStatus(orderId, "Completed");
    }

    private static void validateOrder(int orderId) {
        // 模拟订单验证逻辑
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Order ID " + orderId + " validated.");
    }

    private static void checkInventory(int orderId) {
        // 模拟库存检查逻辑
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Inventory checked for Order ID " + orderId + ".");
    }

    private static void processPayment(int orderId, int orderAmount) {
        // 模拟支付处理逻辑
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Payment processed for Order ID " + orderId + " - Amount: " + orderAmount);
    }

    private static void updateOrderStatus(int orderId, String status) {
        // 模拟订单状态更新逻辑
        System.out.println("Order ID " + orderId + " status updated to " + status + ".");
    }
}

在这个示例中,我们创建了一个固定大小的线程池,其大小等于可用处理器的数量。然后,我们模拟了一个包含100个订单的队列,并为每个订单提交了一个任务到线程池中。每个任务代表订单处理的整个流程,包括验证、库存检查、支付处理和状态更新。每个步骤都通过休眠来模拟耗时操作。最后,我们关闭线程池并等待所有任务完成。

sleep和wait的区别

sleep是Thread类的一个静态方法,它使当前线程暂停执行指定的时间,但不会释放锁资源。
wait是Object类的一个方法,它使当前线程暂停执行释放当前对象上的锁,直到另一个线程调用同一个对象的notify()notifyAll()方法,或者过了指定的等待时间。

sleep是线程内的静态方法 需要指定睡眠的时间 或者自动自己唤醒 不会释放锁
wait是Object类的一个方法 可以指定睡眠时间 不指定就等于无限期 要释放锁
wait一定要搭配synchronized,且都为同一个对象 synchronized锁住了wait万物对象皆为锁
可以被唤醒notify()notifyAll()方法 区别:notify是随机唤醒一个 notifyall会唤醒全部

普通方法上 锁的是this
静态方法上 锁的是当前类的class对象

ConcurrentHashMap 和 HashTable的区别

是否支持传入NULL

HashMap可以支持为null
若尝试将 null 作为键或值放入 ConcurrentHashMap 将会抛出 NullPointerException
ConcurrentHashMap 不能支持存null

底层实现

ConcurrentHashMap 1.8之前是分段锁来实现 默认是16个HashTable
1.8之后无限接近单个的HashMap 底层用CAS+synchronized
HashTable通过加synchronized锁来控制线程安全

ConcurrentHashMap 读不要加锁 [读写的读也不会加锁] 会走最终一致性
HashTable 读要加锁 [读读都加锁]

为什么要用Redis

高性能:Redis是基于内存的数据结构存储,可以提供高速的数据读写操作。
数据结构丰富:Redis支持多种数据结构,如字符串、列表、集合、散列表、有序集合等,非常适合各种场景。
持久化:Redis支持数据持久化,可以将内存中的数据保存到磁盘中,防止数据丢失。
分布式:Redis支持主从复制哨兵集群模式,可以轻松实现分布式缓存。

Redis中缓存了哪些数据

可以存储一下类型的数据
  1. 会话缓存(Session Store):用户会话信息,如用户登录状态、用户偏好设置等。
  2. 页面缓存:动态生成的网页内容,以减少数据库的读取次数。
  3. 对象缓存:例如,用户信息、商品详情等,减少数据库访问。
  4. 消息队列:用作消息队列,处理异步任务。
  5. 排行榜或计数器:如用户点赞数、视频播放次数等。
  6. 地理空间数据:用于实现基于地理位置的查询。
  7. 分布式锁:在分布式系统中协调不同服务或节点的操作
关于您提到的替代JWT的方案,即使用Redis来管理登录状态而不是使用JWT,这里有一些详细说明:

若放登录的信息到Redis的时候 不再用JWT了
Session在集群里面不能用了
替代方案:用Redis 不用JWT
JWT是无状态 无需集中存储

在我们的项目中,Redis中缓存了以下类型的数据:
会话信息:如用户登录信息、购物车内容等。
热点数据:如热门商品信息、推荐内容等。
计数器:如用户访问次数、点赞数、评论数等。
缓存数据库查询结果:减少数据库访问次数,提高系统响应速度。

JWT(JSON Web Tokens)

JWT是一种在各方之间传递安全可靠信息的简洁的、URL安全的表达方式。其特点包括:

  • 无状态:服务器不存储任何会话信息,每个请求都携带包含所有必要信息的JWT。
  • 自包含:JWT中包含了用户的所有声明,减少了服务器的数据库查询。
  • 跨域认证:特别适用于单点登录(SSO)。

JWT的局限性

  • 无法失效:一旦签发了JWT,在它过期之前,它在任何地方都是有效的,无法提前失效。
  • 续签问题:JWT过期后需要重新签发,处理起来相对复杂。
  • 携带信息量大:每个请求都携带JWT,如果JWT中包含的信息较多,会增加请求的大小。

使用Redis替代JWT

使用Redis作为会话存储,可以解决JWT的一些问题:

  1. 中心化控制:通过Redis,服务器可以集中管理会话信息,可以随时使会话失效。
  2. 灵活的过期策略:可以设置更细粒度的过期时间,并在需要时刷新会话。
  3. 状态管理:对于需要频繁更改用户状态的应用,使用Redis可以更方便地管理。
  4. 安全性:虽然Redis存储会话信息,但可以结合HTTPS和合适的加密策略来保证传输过程的安全

实现方案

  1. 用户登录:用户登录成功后,生成一个唯一的会话标识(如UUID),并将其作为key存储在Redis中,value可以是用户ID或者其他必要信息,并设置适当的过期时间。
  2. 请求验证:用户每次请求时,需要在请求头中携带会话标识,服务器端通过这个标识在Redis中查找会话信息,进行验证。
  3. 会话过期或失效:当用户登出或会话过期时,从Redis中删除对应的会话信息。

注意事项

  • 数据持久性:Redis的数据是存储在内存中的,需要考虑持久化策略以防止数据丢失。
  • 高可用性:在集群环境下,需要配置Redis的高可用方案,如哨兵(Sentinel)或集群模式。
  • 安全性:确保Redis的安全性,防止未授权访问。

检测数据存在Redis中,有过期时间吗? 过期时间是多少?仅参考

是的,我们在Redis中缓存的数据通常会设置过期时间,以避免过时的数据占用内存。具体的过期时间取决于数据的类型和业务需求。对于会话信息,我们可能会设置较短的过期时间,如30分钟或1小时;而对于热点数据,可能会设置较长的过期时间,如几小时或一天。具体的过期时间需要根据实际业务场景和数据访问模式来决定。

   // 用户登录,创建会话
    public String loginUser(String userId) {
        String sessionId = UUID.randomUUID().toString();
        String sessionData = createSessionData(userId);
        jedis.setex(sessionId, 1800, sessionData); // 设置会话过期时间为30分钟
        return sessionId;
    }

微服务之间如何调用?

通过注册中心去协调的
首先是有三个重要的概念,服务消费者注册中心服务提供者在第一次会把自己的信息注册到注册中心中,比如ip端口,服务功能等消费者需要到注册中心来寻找服务进行消费,在服务消费者第一次请求的时候会拉取服务提供者的信息,注册中心会把提供者的实例列表给到消费者供消费者选择,使用负载均衡来选择服务,默认为轮询,还有加权轮询,随机。同时服务消费者还会定时去注册中心拉取服务提供者的信息

如果我们的服务挂掉了怎么办?

服务提供者会每隔一段时间去向注册中心报告自己的状态[发送心跳ping 30s/次 共90s],如果没有向注册中心报告状态,那么这个时候注册中心会认为服务提供者已经宕机,同时会推送到我们的服务消费者,这个服务提供者已经宕机

微服务的五大组件

  1. 服务注册与发现:如Eureka已过时Nacos、Consul,用于服务的注册和发现。
  2. 配置管理:如Spring Cloud Config、OpenFeign 用于集中管理服务的配置。

Feign是一个声明式的Web服务客户端(Web服务客户端就是Http客户端),让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可。

OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过【动态代理】的方式产生实现类,实现类中做负载均衡并调用其他服务。

@FeignClient(name = "feignTestService", url = "http://localhost/8001")
public interface FeignTestService {
}

@Component
@FeignClient(url = "http://localhost/8001")
public interface PaymentFeignService{
}
二、OpenFeign使用
2.1.OpenFeign 常规远程调用
所谓常规远程调用,指的是对接第三方接口,和第三方并不是微服务模块关系,所以肯定不可能通过注册中心来调用服务。

第一步:导入OpenFeign的依赖

第二步:启动类需要添加 【@EnableFeignClients】
    
第三步:提供者的接口
@RestController
@RequestMapping("/test")
public class FeignTestController {

    @GetMapping("/selectPaymentList")
    public CommonResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize) {
        System.out.println(pageIndex);
        System.out.println(pageSize);
        Payment payment = new Payment();
        payment.setSerial("222222222");
        return new CommonResult(200, "查询成功, 服务端口:" + payment);
    }

    @GetMapping(value = "/selectPaymentListByQuery")
    public CommonResult<Payment> selectPaymentListByQuery(Payment payment) {
        System.out.println(payment);
        return new CommonResult(200, "查询成功, 服务端口:" + null);
    }

    @PostMapping(value = "/create", consumes = "application/json")
    public CommonResult<Payment> create(@RequestBody Payment payment) {
        System.out.println(payment);
        return new CommonResult(200, "查询成功, 服务端口:" + null);
    }

    @GetMapping("/getPaymentById/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") String id) {
        System.out.println(id);
        return new CommonResult(200, "查询成功, 服务端口:" + null);
    }
    
第四步:消费者调用提供者接口
@FeignClient(name = "feignTestService", url = "http://localhost/8001")
public interface FeignTestService {

    @GetMapping(value = "/payment/selectPaymentList")
    CommonResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);

    @GetMapping(value = "/payment/selectPaymentListByQuery")
    CommonResult<Payment> selectPaymentListByQuery(@SpringQueryMap Payment payment);

    @PostMapping(value = "/payment/create", consumes = "application/json")
    CommonResult<Payment> create(@RequestBody Payment payment);

    @GetMapping("/payment/getPaymentById/{id}")
    CommonResult<Payment> getPaymentById(@PathVariable("id") String id);
}
  1. 服务网关:如Zuul、Spring Cloud Gateway,作为系统的唯一入口,处理外部请求的路由和过滤。
  2. 负载均衡:如Ribbon,用于在多个服务实例之间分配请求。
  3. 断路器:如Hystrix,用于服务熔断,防止系统雪崩

对于服务注册这块有什么了解?

  • 服务注册中心:服务实例在启动时向服务注册中心注册自己的地址和端口信息。检查 心跳 如果未查询就剔除,同时也有注册中心主动发起请求。
  • 健康检查:服务注册中心通常会定期对已注册的服务进行健康检查,以确保服务的可用性。
  • 服务发现:服务消费者通过服务注册中心查找可用的服务实例,以进行服务调用。
  • 服务去注册:当服务实例关闭或出现故障时,它需要从服务注册中心注销,以避免调用不可用的服务。

你能说一下小程序的登录流程吗?

调用微信api,根据code获取openid;根据openid查询用户为空就新增;调用微信api WechatService + WechatServiceImpl(openId+phoneCode) 获取用户绑定的手机号;保存或修改该用户;将用户id存入token返回(JWT生成token)

有哪些方式可以创建单例?

  1. 饿汉式:在类加载时就立即初始化并创建单例对象。
  2. 懒汉式:在第一次调用时初始化单例对象,通常需要考虑线程安全问题。
  3. 双重校验锁:在懒汉式的基础上,通过双重校验锁确保线程安全。
  4. 静态内部类:利用静态内部类的加载机制来确保单例对象的唯一性。
  5. 枚举:利用枚举的特性,保证单例对象的唯一性和线程安全【不可用反射】
并发情况下严格控制单例?volatile→禁止进行指令重排序

双重校验锁:在懒汉式的基础上,通过双重校验锁确保线程安全。

思考一个问题:哪些方式创建单例模式?

1. 懒汉式,线程不安全

这种方式在类加载时不初始化。在需要的时候才创建对象,节约资源。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2. 懒汉式,线程安全

通过同步方法确保线程安全。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3. 饿汉式

类加载时就完成了初始化,保证了线程的安全性。

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

4. 双重校验锁

线程安全且在实例域需要延迟加载时提高性能。

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

5. 静态内部类

这种方式既实现了懒加载,又保证了线性安全。

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

6. 枚举

实现单例的最佳方法,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象。

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

公平锁和非公平锁的区别?

  • 公平锁:多个线程按照它们请求锁的顺序来获取锁,先来先得。这种方式不会产生饥饿现象,但可能会降低吞吐量,因为需要额外的开销来维护等待队列的顺序。【僵尸线程】对资源顺序有严格要求

    为什么会降低吞吐量?
    1. 维护等待队列:公平锁为了保证按照请求锁的顺序来获取锁,必须维护一个有序的等待队列。这意味着每次有线程请求锁或者释放锁时,都需要进行额外的操作来管理这个队列,这会增加开销。
    2. 上下文切换开销:当锁被释放时,公平锁需要唤醒等待队列中的第一个线程。这个过程涉及到线程的上下文切换,上下文切换是有成本的,因为它涉及到保存和恢复线程的状态。
    3. 减少并发机会:由于公平锁要求先来先得,即使锁被释放后请求锁的线程即使处于可运行状态,也必须等待前面的线程先执行。这可能会减少并发执行的机会,从而降低吞吐量。
    4. 增加延迟:公平锁可能会增加线程获取锁的平均等待时间,因为每个线程都必须等待前一个线程完成。这种延迟可能会导致系统整体性能下降。
  • 非公平锁:线程获取锁的顺序不一定是按照请求锁的顺序,允许线程“插队”。这种方式可能会提高吞吐量,但可能导致某些线程长时间无法获取锁,产生饥饿现象。【为什么性能高?不用沉睡和阻塞 避免来回切换】对性能要求高

ReentrantLock 看传参 公平/非公平都支持
ReentrantLock 是Java提供的显式锁,它允许你通过构造函数参数来选择是使用公平锁还是非公平锁:

公平锁(Fair Lock):通过传递true给ReentrantLock的构造函数来创建。这确保了锁的获取是按照线程请求锁的顺序进行的,防止饥饿现象。
ReentrantLock fairLock = new ReentrantLock(true);

非公平锁(Non-Fair Lock):通过传递false或者不传递参数(默认值)给ReentrantLock的构造函数来创建。这种方式允许线程“插队”,可能会提高吞吐量,但也可能导致某些线程长时间无法获取锁。
ReentrantLock nonFairLock = new ReentrantLock(false); // 或者
ReentrantLock nonFairLock = new ReentrantLock(); // 默认是非公平锁
Synchronized 公平锁
从Java 6开始,synchronized的实现得到了改进,它试图实现一种偏向锁和轻量级锁的策略,以提高性能。
以下是关于synchronized的一些关键点:

偏向性:synchronized在锁竞争不激烈的情况下,会尝试偏向第一个获取锁的线程,这有助于减少不必要的同步开销。
轻量级锁:当没有竞争时,synchronized会使用轻量级锁,这比使用操作系统提供的重量级锁要快得多。
重量级锁:当存在竞争时,synchronized会升级为重量级锁,这涉及到操作系统的线程调度。

synchronized不会严格地保证公平性,因为它允许锁的“重入”和偏向性,这意味着它更倾向于非公平锁的行为。然而,在锁竞争激烈的情况下,synchronized会尽量保持一定的公平性,因为它会按照线程在监视器队列中的顺序来获取锁。

总的来说,synchronized不是严格意义上的公平锁,但它的实现细节和调度策略可能会在一定程度上表现出公平锁的特性。与ReentrantLock相比,synchronized的锁获取机制更为复杂,并且它是由JVM内部实现的,因此具体的调度细节对开发者来说是透明的。

SpringMVC的工作原理

  • 前端的HTTP请求到达时首先被DispatcherServlet接收

  • DispatcherServlet根据请求信息路径查找合适的HandlerMapping来确定哪个Controller应该处理该请求。

  • 找到合适的Controller后,DispatcherServlet将请求转发给它处理。

  • Controller处理完请求后返回一个ModelAndView对象给DispatcherServlet。

  • DispatcherServlet再通过ViewResolver解析ModelAndView中的视图逻辑名,找到对应的视图。

  • 最后,DispatcherServlet将模型数据渲染到视图上并响应给客户端。

image-20250206094925513

OpenFeign的底层原理

OpenFeign 实现了简洁、声明式的 HTTP 请求调用,并且与 Spring Cloud 集成后能提供更多的功能如负载均衡等

动态代理: OpenFeign 使用 Java **动态代理技术**,基于接口创建代理类,代理类会自动发起 HTTP 请求。你定义的接口方法会映射到 HTTP 请求上,OpenFeign 会根据注解(如 @RequestMapping, @GetMapping 等)来构建请求。

注解解析: OpenFeign 会解析接口方法上的注解,构造 HTTP 请求的 URL、请求方法类型(GET、POST 等),以及请求体和请求头等信息。

请求拦截和处理: 在请求发起之前,OpenFeign 允许通过拦截器(RequestInterceptor)来修改请求,比如设置请求头、参数等。

负载均衡与容错: 如果与 Spring Cloud 一起使用,OpenFeign 会集成 Ribbon(负载均衡)和 Hystrix(容错),使得服务调用更加健壮和可靠。

序列化与反序列化: OpenFeign 会利用 Jackson 等库进行请求和响应的序列化和反序列化,将 Java 对象与 HTTP 请求/响应内容相互转换

在使用OpenFeign时,开发者只需要定义接口并添加相应的注解,OpenFeign会在运行时动态生成实现类来执行HTTP请求。

对Volatile的理解

volatile 是Java语言中的一个关键字,用于修饰变量,以确保该变量的读写操作对所有线程立即可见,并且防止指令重排序优化。

确保了不同线程对这个变量进行读写操作时的可见性。
是java的关键字是修饰共享的变量,不能修饰局部变量。
修饰普通或静态成员变量,主要用来保证可见性有序性

可见性(Visibility)

在一个多线程程序中,为了提高性能,每个线程可能会将共享变量缓存到自己的CPU缓存中。如果一个线程修改了这个变量的值,而这个新值没有及时写回主内存,那么其他线程可能会读取到旧值。使用volatile关键字可以确保:

  • 每次读写变量都是直接操作主内存。
  • 当一个线程修改了一个volatile变量时,新值会立即被写入主内存。
  • 其他线程读取volatile变量时,会从主内存中读取最新值。

这样,volatile就保证了不同线程之间共享变量的可见性。

有序性(Ordering)

在没有volatile修饰的变量上,Java编译器和处理器可能会进行指令重排序,以提高程序运行的效率。指令重排序可能会导致程序的执行顺序与代码的编写顺序不一致。使用volatile可以防止以下两种类型的重排序:

  • 写操作的重排序volatile变量的写操作不允许与它之前的操作重排序。
  • 读操作的重排序volatile变量的读操作不允许与它之后的操作重排序。

这样,volatile就提供了一定的有序性保证。

Spring Security的实现

我的项目是基于JWT的前后端分离的项目,在自定义认证管理器AuthenticationManager认证成功后,生成JWT令牌并返回给前端。前端在随后的请求中携带这个JWT令牌。这时候,我们使用AccessDecisionManager来实现接口的鉴权逻辑,其中包括一个check方法,该方法会校验JWT令牌的有效性。如果校验通过,就去查询数据库以确定用户拥有哪些权限。在用户登录时,其权限信息已经被缓存到Redis中。后续的请求中,我们可以直接从Redis中检索用户的权限信息。如果请求的接口权限与用户缓存中的权限匹配,则放行;如果不匹配,则返回一个友好的错误信息

什么是AQS

是多线程中的抽象队列同步器。是一种锁机制,它是做为一个基础框架使用的,是一个抽象类。
像ReentrantLock都是基于AQS实现的

在Java的并发编程中,AbstractQueuedSynchronizer(简称AQS)是一个非常重要的类,它提供了一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和其他同步器(例如信号量、事件等)。AQS 本身是一个抽象类,它内部定义了获取资源(锁)释放资源(锁)的基本方法,以及管理同步状态队列的机制。

当说“ReentrantLock是基于AQS实现的”,意味着ReentrantLock这个具体锁的实现类,是继承并利用了AQS提供的模板方法来构建其功能的。具体来说:

  • 继承ReentrantLock内部有一个内部类叫做Sync,这个Sync类直接继承自AbstractQueuedSynchronizer
  • 实现Sync类(及其子类)会根据需要重写AQS的一些方法,如tryAcquiretryRelease,这些方法用于定义获取锁和释放锁的具体行为。
  • 利用模板方法:AQS提供了一系列的模板方法(如acquirerelease等),这些方法内部会调用前面提到的可重写方法(如tryAcquiretryRelease),从而允许ReentrantLock按照特定的逻辑来管理锁的状态。

因此,“基于AQS实现”的表述强调了ReentrantLock并不是从头开始构建锁的所有细节,而是站在AQS这个强大的基础框架之上,通过实现特定的策略来完成锁的具体功能。这样做的好处是减少了代码量,提高了代码的可维护性和可重用性,并且由于AQS经过了严格的测试,基于它实现的锁也更加可靠。

定义了一个并发情况下一些抽象的资源 资源能否共享/独享 定义了公平/非公平
如果是非公平锁如果来了个新的线程来抢线程 也是会去抢一次
AQS成为了JUC很多类都去继承的 它抽象了很多并发的属性和行为,让子类去继承它扩展自己

Synchronized的锁升级

  • Monitor实现的锁属于重量级锁,里面涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低
  • 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题

一段很长的时间内都只被一个线程使用锁 偏向锁
有线程交替或线程加锁的时间是错开的 轻量级锁
有很多线程来抢 重量级锁

偏向锁

偏向锁是一种优化锁的机制,它的设计初衷是:如果一个锁在大多数时间里只被一个线程访问,那么就没有必要进行线程间的同步操作,因为不存在锁竞争。在这种情况下,JVM会为这个锁赋予一个“偏向”,即偏向于第一个获取它的线程。在后续的锁操作中,如果该线程再次请求这个锁,就不需要进行同步操作,从而提高性能。偏向锁适用于只有一个线程访问同步块的场景。

轻量级锁

轻量级锁是另一种锁的优化,它适用于锁竞争不是很激烈,且锁持有的时间短的场景。当线程交替访问同步块时,使用轻量级锁可以减少传统的重量级锁带来的性能开销。轻量级锁是通过在对象头中的一些标记位来实现的,当锁处于轻量级锁状态时,线程通过CAS操作来尝试获取锁,如果成功,则直接进入同步块执行,从而避免了使用操作系统级别的重量级锁机制。

重量级锁

重量级锁是JVM中最传统的锁实现,也是性能开销最大的锁。当有很多线程同时竞争同一个锁时,JVM会使用重量级锁来确保线程安全。重量级锁依赖于操作系统的互斥量(mutex),会导致线程状态在用户态和核心态之间转换,这种转换是非常耗时的。因此,当锁竞争非常激烈时,使用重量级锁可以保证公平性和线程安全,但会带来较大的性能开销。

在处理多线程同时竞争同一个锁的情况时,并不一定总是需要使用重量级锁。以下是一些优化和策略,可以帮助您更好地处理并发场景:

  1. 最小化同步范围
    • 仅对必要的代码块进行同步,减少锁的竞争。
    • 使用细粒度锁,比如对不同的数据结构使用不同的锁,而不是对整个对象加锁。
  2. 使用并发工具类
    • Java提供了许多并发工具类,如java.util.concurrent包中的ReentrantLockReadWriteLockSemaphoreCountDownLatchConcurrentHashMap等,它们提供了比synchronized更丰富的功能。
  3. 锁分离
    • 对于读多写少的场景,可以使用读写锁(ReadWriteLock),它允许多个读线程同时访问,而写线程则互斥。
  4. 锁优化
    • 在锁竞争不是很激烈的情况下,可以使用轻量级锁或偏向锁,这些锁的开销比重量级锁小。
  5. 无锁编程
    • 使用原子类(如AtomicIntegerAtomicReference)和线程安全的数据结构,这些类通过CAS操作实现了无锁的线程安全。
  6. 线程池
    • 使用线程池来管理线程,避免频繁创建和销毁线程带来的开销。
  7. 避免死锁
    • 设计代码时注意锁的顺序,避免循环等待条件,减少死锁的发生。
  8. 性能测试
    • 对并发代码进行性能测试,了解不同锁策略对性能的影响,并根据测试结果选择合适的锁。

java语言是高级语言如果想调用底层的操作系统和硬件要通过操作系统的API去操作。以前老的JDK版本 数据是在操作系统找的数据,Monitor的标志0 和 1,底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低 。
引入新型锁后,java里是用对象头找个地方存一把锁,这样就不涉及到调用操作系统底层。一开始new了个对象 此时是无锁状态。接下来来人拿锁,长期一个人拿到那个锁 此时是偏向锁竞争不激烈。后面多线程一起来 交替抢锁 此时是轻量级锁。随着并发越来越高 此时在一个线程拿到锁后很多线程来抢锁 线程先尝试自己先获取几次(自旋锁64次未拿到锁就会升级为重量级锁) 这时就涉及到操作系统的底层对象涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低。锁不可逆可能新版本可以降级

Dockerfile 常用命令

  • FROM: 指定基础镜像。
  • ENV: 设置环境变量。
  • RUN: 执行命令并创建新的镜像层。
  • COPY: 将文件从宿主机复制到容器中。
  • EXPOSE: 声明容器运行时将监听的端口。
  • ENTRYPOINT: 配置容器启动时运行的命令。

常用的 Docker 命令

  • docker run: 创建一个新的容器并运行一个命令。
  • docker pull: 从仓库中拉取或者更新一个镜像。
  • docker push:推送镜像到服务
  • docker build: 从 Dockerfile 构建一个镜像。
  • docker images: 列出本地镜像。
  • docker ps: 列出运行中的容器。
  • docker stop: 停止一个运行中的容器。
  • docker start: 启动一个停止的容器。
  • docker rm: 删除一个容器。
  • docker rmi: 删除一个镜像。
  • docker exec: 在运行中的容器内执行命令。
  • docker logs: 获取容器的日志。
- docker volume create:创建数据卷
- docker volume ls:查看所有数据卷
- docker volume inspect:查看数据卷详细信息,包括关联的宿主机目录位置
- docker volume rm:删除指定数据卷

Docker Compose 常用命令

  • docker-compose up: 启动所有服务的容器。
  • docker-compose down: 停止并删除容器、网络、卷和镜像。
  • docker-compose ps: 列出项目中所有的容器。
  • docker-compose exec: 进入指定的容器。
  • docker-compose build: 构建或重建服务。
  • docker-compose logs: 查看服务的日志输出。
  • docker-compose stop: 停止运行的容器。

synchronized 和 ReentrantLock 的区别

  • synchronized 是Java的一个关键字用于方法和代码块中,而 ReentrantLock 是JUC包的一个类。
  • synchronized 可以自动加锁和解锁,而 ReentrantLock 需要手动加锁和解锁。
  • synchronized 的锁是非公平的,而 ReentrantLock 默认也是非公平的,但可以设置为公平锁。

你们公司是怎么部署项目的

是通过docker + jenkins
测试环境我们参与 生产环境组长部署

varchar 与 char 区别

  • varchar 是可变长度的字符串,而 char 是固定长度的字符串。
  • varchar 的性能通常比 char 差,因为需要处理额外的长度信息。
  • 当数据长度变化很大时,推荐使用 varchar;当数据长度几乎固定时,使用 char 可能更合适。

Redis的持久化有哪几种? 它们的区别是什么?

完整性 大小 数据恢复速度 建议

Redis持久化:RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,数据恢复。

[root@localhost ~]# redis-cli
127.0.0.1:6379> save          #由Redis主进程来执行RDB,会阻塞所有命令
ok

127.0.0.1:6379> bgsave        #开启子进程执行RDB,避免主进程受到影响
Background saving started

Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

// 900秒内,如果至少有1个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000

==RDB的执行原理?==数据完整性高用RDB

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件

在LInux中主进程并无法直接读取物理内存,它只能通过虚拟内存去读。因此有页表(记录虚拟地址与物理地址的映射关系)去执行操作 同时 主进程也会fork(复制页表) 成为一个新的子进程(携带页表) → 写新RDB文件替换旧的RDB文件 → 磁盘

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

优点:二进制数据重启后 Redis无需过多解析 直接恢复

==AOF==对数据不敏感要求不高

AOF全称为Append Only File(追加文件)底层硬盘顺序读写。Redis处理的每个写命令都会记录在AOF,可以看作是命令日志文件
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录的频率也可以通过redis.conf文件来配

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完毕先放入AOF缓冲区,然后表示每隔一秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 刷盘时机 优点 缺点
Always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
everysec 每秒刷盘 性能适中 最多丢失1秒数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义通过执行bgrewriteaof命令,可以让AOF文件执行重读功能,用最少的命令达到相同效

Redis会在出发阈值时自动重写AOF文件。阈值也可以在redis.conf中配置

# AOF文件比上次文件 增多超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

★★★★★★★★ RDB与AOF对比 ★★★★★★★★

RDB和AOF各有优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
RDB是二进制文件,在保存时体积较小恢复较快,但也有可能丢失数据,我们通常在项目中使用AOF来恢复数据,虽然慢但丢失数据风险小,在AOF文件中可以设置刷盘策略(每秒批量写入一次命令)

RDB AOF
持久化方式 定时对整个内存做快照哦 记录每一次执行的命令
数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
文件大小 会有压缩,文件体积小 记录命令,文件体积大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源
但AOF重写时会占用大量CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见

项目中是怎么使用redis的

需要结合项目中的业务进行回答,通常情况下,分布式锁的使用场景:
集群情况下的定时任务、抢单、幂等性场景
如果使用互斥锁的话 那么在集群项目有多个服务器就会出现问题

用Hash类型 大Key是Id 小key是商品id value是商品数量
数据量点击量 用String类型
用Set类型 Zset做排行榜

你的项目中哪里使用到分布式锁?

==Redis分布式锁==

Redis实现分布式锁主要利用Redis的setnx命令,setnx是**SET if not exists**(如果不存在,则SET)的简写

  • 获取锁

    添加锁,NX是互斥、EX是设置超时时间
    SET lock value NX EX 10

  • 释放锁

    释放锁,删除即可
    DEL key

你可以说一下redis的分布式锁的原理吗

我在项目中是集成了redisson(底层基于Lua脚本[具有原子性])

==redisson实现分布式锁 - 执行流程==

加锁 ↓→ 加锁成功 → Watch dog(看门狗)每隔(releaseTime/3的时间做一次续期) → Redis
↓ 操作redis → Redis
↓→→ 释放锁↑ → 通知看门狗无需继续监听 → Redis

加锁 → → → 是否加锁成功?→→→ ↓
↑←←while循环不断尝试获取锁←←←↓

public void redisLock() throws InterruptedException{
    RLock lock = redissonClient.getLock("heimalock");
 // boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 如果不设置中间的过期时间30 才会触发看门狗
// 加锁,设置过期时间等操作都是基于lua脚本完成的[调用redis命令来保证多条命令的原子性]
    boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
    if(isLock){
        try{
            sout("执行业务");
        } finally{
            lock.unlock();
        }
    }
}

==redisson实现分布式锁 - 可重入==

redis实现分布式锁是不可重入的 但是 redisson实现分布式锁是可以重入的
可重入原理:它俩是同一个线程 每个线程都有唯一的线程id 根据线程id唯一标识做判断 判断之前获取锁是不是同一个线程
利用hash结构记录线程id重入次数

KEY VALUE VALUE
field value
heimalock thread1 0
public void add1(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
  add2();
// 释放锁
  lock.unlock();
}
public void add2(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
// 释放锁 锁次数-1不完全释放
  lock.unlock();
}

==redisson实现分布式锁 - 主从一致性==

Redis Master主节点:主要负责写操作(增删改) 只能写
Redis Slave从节点:主要负责读操作只能读

当RedisMaster主节点突然宕机后 Java应用会去格外获取锁 这时两个线程就同时持有一把锁 容易出现脏数据
怎么解决呢?

  • RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例【实现复杂、性能差、运维繁琐】怎么解决?→ CP思想zookeeper

redis和mysql怎么保证数据一致性

先插入数据库
更新先更新数据库 更新数据库成功但redis不成功 影响不大 因为后面会有过期删除 最终会一致,更新mysql后缓存可以删除也可以修改
更新完数据库直接删除缓存了 有过期时间兜底 最终会保持一致 我们项目中对数据敏感性一致性不高 我们追求实时性
如果是最终保持一致性的就MQ 我们对实时性不高 对数据敏感性 一致性高
删除问题不大 哪里都行!
读多写少的可以上缓存
mysql保存购物车表 但是再页面操作的时候 只操作redis 用mq给到消费者修改或定时任务 更新数据到mysql,MQ问题:我们对数据实时性要求不高 只需要保存最终一致性就行
你如果只写redis 万一丢了数据怎么办
购物车丢点订单无影响 数据安全性要求不太高 mysql尽量不要搞购物车的表 都在redis的表 丢就丢了呗。或者异步同步/定时任务
实时性要求 安全性要求 → MySQL
电商一般数据库和mysql都要存 → 读多写少

一定、一定、一定要设置前提,介绍自己的业务背景 (一致性要求高?允许延迟一致?)

① 介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以我们采用的是异步的方案同步的数据

② 我们当时是把抢卷的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致性,我们当时采用的是redission提供的读写锁来保证数据的同步

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

  • 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间

  • 写操作:延迟双删 [因为无论先删除缓存还是先删除数据库都可能会出数据不一致问题 有脏数据]

  • ==基于redisson互斥锁:==[放入缓存中的数据 读多写少] 【强一致性业务 性能低】有过期时间兜底

    • 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,但**不允许写操作**
    • 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作(只允许一个用户或进程独占地对数据进行读取和写入操作)排他锁确保了写操作的原子性和一致性
    • 读数据的时候添加共享锁(读不互斥、写互斥)
    • 写数据的时候添加排他锁(阻塞其他线程的读写 因为读多写少)

    redissionClient.getReadWriteLock(“xxxx”);

  • ==异步通知:==异步通知保证数据的最终一致性(需要保证MQ的可靠性)需要在Redis中更新数据的同时,通知另一个服务进行某些操作。

    • 使用场景
      • 缓存与数据库双写: 当应用需要同时更新Redis缓存和数据库时,可以先将数据写入Redis,然后通过异步通知机制触发数据库的更新操作。
      • 跨地域数据复制: 在跨地域部署的服务中,为了实现数据的最终一致性,可以在一个地域写入数据后,通过异步通知机制在另一个地域进行数据复制。
      • 系统间数据同步: 在微服务架构中,不同的服务可能有自己的数据存储。当一个服务更新了数据后,可以通过异步通知机制告知其他相关服务进行数据同步。
  • ==基于Canal的异步通知==:监听mysql的binlog
    可以解析binlog文件 可以存放mysql里面的数据 看最近有无增删改查 转换成redis命令 再给redis里面

    • 使用MQ中间件,更新数据之后,通知缓存删除
    • 利用canal中间件,不需要修改业务代码,伪装为mysqls的一个从节点,canal通过读取binlog数据更新缓存

synchronized可以作用在哪些地方(作用域),分别锁的是什么

在Java中,synchronized关键字可以用来实现线程同步,它可以作用在不同的地方,并且锁定的对象也不同:

  1. 实例方法

    • 作用在实例方法上时,锁的是调用该方法的对象实例(即**this对象**)。
    • 任何线程想要执行这个方法,都必须获得该对象实例的锁。
    public synchronized void synchronizedMethod() {
        // 方法体
    }
    
  2. 静态方法

    • 作用在静态方法上时,锁的是类的Class对象
    • 由于静态方法是属于类的,而不是属于任何特定实例,所以所有线程要想执行这个静态同步方法,都必须获得该类的Class对象的锁。
    public static synchronized void synchronizedStaticMethod() {
        // 方法体
    }
    
  3. 代码块

    • 作用在代码块上时,可以指定一个锁对象括号里的对象,可以是任何对象
    • 当进入这个代码块时,线程必须获得指定锁对象的锁。
    public void synchronizedBlock() {
        synchronized(this) { // 锁定当前对象实例
            // 代码块
        }
    }
    
    public void synchronizedBlockWithObject() {
        Object lock = new Object();
        synchronized(lock) { // 锁定指定的对象
            // 代码块
        }
    }
    

什么情况下索引会失效?

  • 违反最左前缀法则
  • 范围查询右边的列,不能使用索引
  • 不要在索引列上进行运算操作,索引将失效
  • 字符串不加单引号,造成索引失效。(类型转换)
  • 以%开头的Like模糊查询,索引失效
    [不影响正常查询业务 但未运用超大分页查询优化 会导致索引失效]

索引创建原则有哪些?索引很多就会有很多B+树

数据量较大,且查询比较频繁的表
常作为查询条件、排序、分组的字段 [where、group by、order by]
③ 字段内容区分度高
④ 内容较长,使用前缀索引
尽量联合索引对存储节省空间

如果我们经常根据客户ID和订单日期来查询订单,那么可以在 customer_id 和 order_date 上创建一个联合索引。
CREATE INDEX idx_customer_date ON orders (customer_id, order_date);

这个联合索引 idx_customer_date 有以下几个特点:

索引顺序:首先根据 customer_id 排序,然后在每个 customer_id 的基础上根据 order_date 排序。
查询优化:以下查询可以利用这个联合索引:
SELECT * FROM orders WHERE customer_id = ? AND order_date = ?;
SELECT * FROM orders WHERE customer_id = ?;

要控制索引的数量
⑦ 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它
大字段不建议建立索引是因为B+树一个叶子节点/一个非叶子节点 差不多16k 一个节点对应一个[页] 多的话会更多层
尽量不用性别去创建索引

  • 先陈述自己再实际工作中是怎么用的
  • 主键索引
  • 唯一索引
  • 根据业务创建的索引(复合索引)

索引的底层数据结构了解过吗?

MySQL的InnoDB引擎采用的B+树的数据结构来存储索引

  • 阶数更多,路径更短
  • 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
  • B+树便于扫库和区间查询,叶子节点是一个双向链表

**MySQL默认使用的索引底层数据结构是B+树**。再聊B+树之前,先来聊聊二叉树和B树

==B Tree(矮胖树)==,B树是一种多叉路衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key

==B+Tree== 是再BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是B+Tree实现其索引结构

B树与B+树对比

  • 磁盘读写代价B+树更低
  • 查询效率B+树更加稳定
  • B+树便于扫库和区间查询

B树要找12 首先找38 左面小 再去缩小范围16和29 找到12 → 但是我们只想要12的数据 B树会额外的把38,16,29的数据全查一遍最后才到12的数据

B+树是在叶子节点才会存储数据,在非叶子节点全是指针,这样就没有其他乱七八糟的数据影响 。且查找路径是差不多的,效率较稳定

便于扫库:比如我们要查询6-34区间的数据,先去根节点扫描一次38 → 16-29 → 由于叶子节点之间有双向指针,就可以一次性把所有数据都给拿到[无需再去根节点找一次]

mysql底层为什么用B+树利用二分查找,树越矮经过磁盘IO次数越少,它是稳定的每次都查到最底层

二叉树 O(logn) 容易退化成链表 所以不用它
平衡二叉树 全部倾斜
红黑树 一个节点只能存一个数据
B树能不能除了叶子节点其他不存数据呢?

你可以设计一种变体的B树,其中只有叶子节点存储数据,而所有其他非叶子节点仅作为导航节点,不存储实际的数据。这种结构在概念上类似于B树的一个特例,通常被称为B树索引结构的一部分,其中非叶子节点存储的是键值,而叶子节点存储的是实际的数据记录或者是指向数据记录的指针

B+树第三层2000多万条数据,尽量不要把数据达到2000多万
B+树叶子节点加了双向链表 让我们查询更加稳定 范围查询会更快

mysql索引底层不一定只有B+树,也可能是Hash 在精准查询性能比它高

R—Tree:地理位置搜索

联合索引

where b= AND c= AND a= 这样走索引都能走 底层自己排序
为什么联合索引要遵循最左匹配原则【里面的b+树 先按照a排序 再b 因为要二分查找 不排序怎么找?】

在MySQL中,如何定位慢查询?查询前用explain查询是否走了索引等问题

explain查询后的列:
id:查询中SELECT语句的序列号,如果该行引用其他行的并集结果,则该值可以为空。
select_type:表示查询的类型,常见的类型有:
SIMPLE:简单的SELECT查询,不使用UNION或子查询。
PRIMARY:最外层的SELECT查询。
UNION:在UNION中的第二个或随后的SELECT查询。
DEPENDENT UNION:在UNION中的第二个或随后的SELECT查询,取决于外层查询。
UNION RESULT:UNION的结果。
SUBQUERY:子查询中的第一个SELECT。
DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外层查询。
table:查询的是哪个表。
type:这是你提到的一个非常重要的列,它表示MySQL在表中找到所需行的方式,也称为“访问类型”。以下是一些常见的访问类型,从最好到最差排序:
system:表只有一行(系统表)。
const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,所以优化器的其余部分可以将这一行视为常量。
eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这通常是最好的联接类型,除了const类型。
ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。
fulltext:使用全文索引执行查询。
ref_or_null:与ref类似,但是MySQL会额外搜索包含NULL值的行。
index_merge:表示查询使用了两个或更多的索引。
unique_subquery:用于IN子查询,子查询返回不重复的值集。
index_subquery:用于IN子查询,子查询返回不重复的值集,可以使用索引。
range:使用索引来检索给定范围的行。
index:全索引扫描(比ALL快,因为索引通常比数据行小)。
ALL:全表扫描,这是最差的一种类型,因为MySQL必须检查每一行以找到匹配的行。
possible_keys:指出MySQL能使用哪些索引来优化查询。
key:MySQL实际决定使用的索引。
key_len:使用的索引的长度。越短越好。
ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:MySQL认为必须检查的用来返回请求数据的行数。
filtered:显示了通过条件过滤出的行数的百分比估计。
Extra:包含MySQL解析查询的额外信息,例如是否使用了索引,是否排序了结果,是否使用了临时表等

1.介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟)
2.我们系统中当时采用了运维工具(Skywalking),可以监测出哪个接口,最终因为是sql的问题
3.在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段)

产生原因:

  • 聚合查询
  • 多表查询
  • 表数据量过大查询
  • 深度分页查询

方案一:==开源工具==[调试阶段才会开启 生产阶段不会开启]

  • 调试工具Arthas
  • 运维工具:Prometheus、SKywalking(接口访问时间)

方案二:==MySQL自带慢日志==

慢查询日志记录了所有执行时间超过指定参数(long_query_time, 单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置信息:

# 开启MySQL慢日志查询开关
slow_query_log = 1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会被视为慢查询,记录慢查询日志
long_query_time = 2

什么是聚簇索引?什么是非聚簇索引(二级索引)?什么是回表?

  • 聚簇索引(聚集索引):数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个【id存放的b+树】
  • **非聚簇索引(二级索引)**:数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个【叶子就是id的字段】
  • 回表查询:通过二级索引找到对应的主键值,到聚集索引中查找正行数据,这个过程就是回表

怎么避免回表 → 使用覆盖索引!
需要name 直接 select name 而不用 select *
要按需来查找

除了InnoDB,MySQL数据库还支持多种其他存储引擎,其中最著名的是MyISAM。以下是InnoDB和MyISAM两个存储引擎的主要区别:

事务支持:
InnoDB:支持事务,它遵循ACID原则(原子性、一致性、隔离性和持久性)。如果事务中的某个操作失败,整个事务可以回滚到开始状态。
MyISAM:不支持事务,这意味着你无法回滚操作,这对于数据完整性和恢复可能是一个问题。
    
锁定机制:
InnoDB:使用行级锁定,只锁定需要的特定行,这可以大大减少数据库操作的冲突。
MyISAM:使用表级锁定,每次操作都会锁定整个表,这在并发操作较多时可能导致性能问题。
    
崩溃恢复:
InnoDB:具有自动崩溃恢复功能,即使数据库崩溃,也不会丢失数据,因为它将事务日志写入磁盘。
MyISAM:在崩溃后恢复较为困难,可能会丢失数据,因为它不记录事务日志。
    
全文搜索:
InnoDB(MySQL 5.6及以后版本):支持全文索引,但功能上不如MyISAM的全文搜索强大。
MyISAM:提供了更强大的全文搜索功能,但在MySQL 5.6之前,这是MyISAM相对于InnoDB的主要优势。
    
存储限制:
InnoDB:表的大小理论上受限于操作系统的文件大小限制,通常可以处理更大的数据量。
MyISAM:表的大小受限于最大文件大小,通常是2GB到4GB,这取决于文件系统的限制。
    
外键支持:
InnoDB:支持外键约束,这有助于保持数据的引用完整性。
MyISAM:不支持外键约束。
    
存储空间:
InnoDB:通常需要更多的存储空间,因为它存储了额外的信息来支持事务和行级锁定。
MyISAM:通常占用更少的存储空间,因为它不需要存储这些额外的信息
分类 含义 特点
==聚集索引(Clustered Index)== 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 必须有, 而且只有一个
==二级索引(Secondary Index)== 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一 (UNIQUE) 索引作为聚集索引
  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引

==回表查询==

select * from user where name = 'Arm';

MySQL的日志文件有哪些,它们的作用是什么

MySQL的日志文件有哪些,它们的作用是什么?

MySQL主要有以下几种日志文件:

  • 错误日志(Error Log):记录MySQL服务的启动、运行或停止过程中的错误信息。
  • 查询日志(General Query Log):记录所有MySQL执行的SQL命令,无论这些命令是否正确执行。
  • 慢查询日志(Slow Query Log):记录执行时间超过指定阈值的查询语句。
  • 二进制日志(Binary Log)记录所有更改数据的SQL语句,用于主从复制和数据恢复。事务的提交 和 主从复制
  • 事务日志/重做日志(InnoDB Redo Log)记录InnoDB存储引擎的事务操作,用于崩溃恢复。
  • 回滚日志/撤销日志(InnoDB Undo Log)用于事务回滚,保证事务的原子性。

undo log 和 redo log的区别?

redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
redo log 保证了事务的持久性,undo log保证了事务的原子性和一致性

  • 缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改査操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
  • 数据页(page):是InnoD8 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据

==redo log==

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
该日志文件由两部分组冲:重做日志缓冲(redo log buffer) 以及 **重做日志文件(redo log file)**,前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都保存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

==undo log==

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚MVCC(多版本并发控制)。undolog 和 redolog记录物理日志不一样,它是逻辑日志

  • 可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然
  • 当update一条记录时,它记录一条对应相反的update记录。当执行rolback时,就可以从undolog中的逻辑记录读取到相应的内容并进行回滚。

undo log可以实现事务的一致性和原子性

MySQL主从同步原理?

MySQL主从复制的核心就是二进制日志binlog[DDL(数据定义语言)语句DML(数据操纵语言)语句]
主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log。
从库重做中继日志中的事件,将改变反映它自己的数据。

主服务器(Master)上的数据更改(如INSERT、UPDATE、DELETE操作)会被记录到二进制日志中。
从服务器(Slave)上的I/O线程连接到主服务器,请求主服务器上的二进制日志。
主服务器将二进制日志发送给从服务器,从服务器将这些日志事件写入到本地的中继日志(Relay Log)。
从服务器上的SQL线程读取中继日志中的事件,并在本地执行这些事件,从而实现数据的复制。

MySQL主从复制的核心就是二进制日志

二进制文件(BINLOG) 记录了所有的DDL(数据定义语言)语句DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句

复制分成三步:
  • Master主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中
  • 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
  • slave重做中继日志中的事件,将改变反应他自己的数据

项目中哪里涉及到分布式事务问题? 你是怎么解决的? 能说一下

分布式事务问题可能出现在跨多个服务或数据库的操作中,例如在订单服务中同时更新订单状态和扣减库存。秒杀案例:是先扣库存提前缓存到redis里,判断够不够,用RabbitMQ异步下来

解决方案:可以使用分布式事务框架,如Seata,其中AT模式是一种常见的解决方案。

AT模式原理:

  • AT模式基于两阶段提交,分为两个阶段:一阶段 prepare 和二阶段 commit/rollback。
  • 在业务方法开始时,Seata会拦截业务SQL,记录业务数据在执行前后的镜像,生成行锁。
  • 如果一阶段 prepare 成功,则二阶段进行 commit,直接提交事务;如果 prepare 失败,则执行 rollback,利用之前保存的数据镜像回滚到执行前的状态。

面试答题模板:项目中如何解决分布式事务问题

1. 在哪些地方遇到分布式事务?

在项目中,当一个操作需要同时修改多个服务或数据库中的数据时,就涉及到分布式事务。

举个例子,比如:

  • 用户下单时,同时需要更新订单服务库存服务账户服务
  • 秒杀系统中,用户提交订单 → 扣减库存 → 支付 → 发货,这些服务彼此独立,不能直接使用本地事务
  • 使用了微服务架构(Spring Cloud 或 Dubbo),服务之间通过 RPC 或 MQ 通信

2. 为什么不能用本地事务?

因为每个服务有独立的数据源和事务管理器,Spring 或数据库事务不能跨 JVM、不能跨库。使用 @Transactional 只对一个服务内部有效。


3. 怎么解决的?你项目中用的什么方式?

我们使用了 Seata 分布式事务框架,配合 MySQL + Nacos 注册中心 + Spring Cloud Alibaba

✅ 主要使用的是 AT 模式

  • 一阶段(try):执行业务逻辑(如插入订单、扣库存),Seata 会自动代理数据源,记录前镜像后镜像,并生成全局锁,挂起全局事务。
  • 二阶段 commit:如果所有子事务都执行成功,Seata 释放资源,数据自动提交。
  • 二阶段 rollback:如果其中一个失败,Seata 会根据前镜像自动回滚其他操作。

项目中哪里用到MQ,用来干什么?

使用 MQ 的主要场景

我们项目中使用了 MQ 来实现一些异步解耦、流量削峰、延迟处理的需求,典型场景包括:

  • 下单成功后异步发放优惠券和积分
  • 订单支付成功后通知物流发货
  • 延迟关闭未支付订单(延迟队列)

具体业务流程(订单 + 积分 + 优惠券)

📦 用户下单 → 支付成功 → MQ 异步通知:

  1. 用户提交订单,订单服务创建订单。

  2. 用户完成支付,订单服务更新订单状态为“已支付”

  3. 然后将订单信息封装成消息,发送到多个 Topic:

    {
      "orderId": "123456789",
      "userId": "user123",
      "status": "PAID"
    }
    
    • 发给 coupon_topic:异步发放优惠券
    • 发给 point_topic:异步发放积分
    • 发给 logistics_topic:异步准备发货

为什么使用 MQ?

目的 说明
解耦服务 各个服务(积分/优惠券/物流)独立开发部署,订单服务只需发送消息,不关心接收者
异步处理 订单服务快速响应用户请求,提升用户体验
流量削峰 高并发秒杀场景中,通过 MQ 缓冲请求,避免库存服务被直接打爆
延迟处理 通过延迟消息机制实现“30分钟未支付自动关闭订单”等功能

示例:异步发放积分与优惠券

🛠 技术栈

  • 消息中间件:RocketMQ / RabbitMQ / Kafka(我使用的是 RocketMQ)
  • 消息格式:JSON
  • 发送方式:异步发送 sendAsync 或可靠发送 sendSync

👇 示例代码:

订单服务 - 发送消息

OrderMessage message = new OrderMessage(orderId, userId, "PAID");
rocketMQTemplate.syncSend("point_topic", message);
rocketMQTemplate.syncSend("coupon_topic", message);

积分服务 - 监听消息

@RocketMQMessageListener(topic = "point_topic", consumerGroup = "point_group")
public class PointConsumer implements RocketMQListener<OrderMessage> {
    public void onMessage(OrderMessage msg) {
        pointService.addPoint(msg.getUserId(), 100);
    }
}

延迟队列:关闭未支付订单

  • 下单成功但未支付时,发送一条延迟消息到 order_timeout_topic,延迟30分钟。
  • 消费时判断订单状态是否已支付,未支付则关闭订单并恢复库存。
Message message = MessageBuilder.withPayload(orderId)
    .setHeader(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 16) // 延迟30分钟
    .build();
rocketMQTemplate.syncSend("order_timeout_topic", message);


问:mq发送的消息 那怎么样才是对接到了真正的业务代码 比如我30分钟没付款 那就给我关闭未支付订单 我这里是mq不只是发送消息的吗 又怎么能触碰到真实的调用关闭未支付订单的那个业务逻辑代码呢?

✅ 所以完整流程如下:

场景:30分钟内未支付 → 自动关闭订单(延迟队列)


✅ 1. 下单成功 → 发一条延迟消息 到 MQ

这一步由订单服务完成,它只是把订单ID封装成消息,发到某个延迟Topic,比如 order_timeout_topic

Message<String> message = MessageBuilder.withPayload(orderId)
    .setHeader(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 16) // 延迟30分钟
    .build();

rocketMQTemplate.syncSend("order_timeout_topic", message);

✅ 2. 等待30分钟,消息达到时间被投递 → 触发消费者

这一步由你自己写的消费者类完成。

@RocketMQMessageListener(
    topic = "order_timeout_topic", 
    consumerGroup = "order_timeout_group"
)
@Component
public class OrderTimeoutConsumer implements RocketMQListener<String> {

    @Autowired
    private OrderService orderService;

    @Override
    public void onMessage(String orderId) {
        // ✅ 真正的业务逻辑调用就在这里
        orderService.closeUnpaidOrder(orderId);
    }
}

✅ 3. 消费者接收到消息,调用你的真实业务代码

public void closeUnpaidOrder(String orderId) {
    Order order = orderMapper.selectById(orderId);
    
    // ✅ 校验状态:只有未支付才关闭
    if ("UNPAID".equals(order.getStatus())) {
        // 更新订单状态为“已关闭”
        order.setStatus("CLOSED");
        orderMapper.updateById(order);

        // 回滚库存(如果之前预扣了库存)
        stockService.rollbackStock(order.getItems());

        log.info("订单 {} 已超时未支付,自动关闭", orderId);
    }
}

✅ 总结一句话:

MQ 只是“消息的搬运工”,但你要写消费者类(RocketMQListener / @RabbitListener / KafkaListener),onMessage() 方法中调用你的业务代码,才算真正“对接了业务逻辑”。


📌 这样你就能实现:

  • 下单时发送一条“30分钟后的提醒消息”
  • 30分钟后,消息被自动投递
  • 消费者接收消息
  • 消费者去执行你写好的“关闭订单”逻辑

你写的业务代码只是“收到通知之后”执行

是 MQ 延迟队列在背后帮你等待 30 分钟

🔁 两种“定时任务”的方式对比

实现方式 谁来等待30分钟? 缺点 优点
定时任务 Cron 你写一个 @Scheduled 每隔1分钟轮询数据库 数据多了会导致 扫描性能差,且 不精确 简单、直观
MQ 延迟队列 MQ自动等待30分钟后投递消息 要配合消息系统、消费者写法略复杂 精准、性能高、无感知、无需数据库轮询

🧩 真实执行流程再解释一遍

🔵 用户下单成功

业务代码执行完后做两件事:

  1. 把订单状态写入数据库:UNPAID
  2. 向 MQ 发一条“延迟30分钟”的消息,内容是订单ID

🟡 MQ 延迟队列(如 RocketMQ)

你设置了:

.setHeader(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 16); // 表示延迟30分钟

MQ 会帮你缓存这条消息,不会立即投递!


🔴 等到 30 分钟后,MQ 才把这条消息交给你写的消费者类(比如 OrderTimeoutConsumer

你的消费者代码才会被执行:

public void onMessage(String orderId) {
    orderService.closeUnpaidOrder(orderId);
}

此时才会调用你写的业务逻辑代码,检查订单是否已支付,如果没支付,就自动关闭

✅ 为什么不用 @Scheduled

你当然可以用定时任务,比如:

@Scheduled(cron = "0 */1 * * * ?")
public void scanUnpaidOrders() {
    // 每1分钟检查一次数据库里状态为 UNPAID 且超过30分钟的订单
}

❗但是问题是:

  • 每次都要 扫整个表 或者一部分数据,性能差
  • 不能做到秒级准确执行(可能要等1分钟才触发)
  • 高并发系统(像电商、秒杀)根本扛不住频繁轮询

✅ 为什么用 MQ 延迟消息是最佳选择?

  • MQ自己维护延时,精确到秒级
  • 不依赖数据库定时轮询
  • 一条消息就是一笔订单,到时间再投递,性能高
  • 和核心业务逻辑完全解耦

异步发优惠卷 + 积分 [用户对于实时性要求不是很高]

  • 使用MQ的场景:订单处理
    具体场景

    当用户在电子商务平台上成功下单后,订单服务需要执行以下操作:

    1. 更新订单状态为“已支付”。
    2. 扣减商品库存。
    3. 通知支付服务处理支付。
    4. 通知物流服务准备发货。
    使用MQ的原因

    在这些操作中,更新订单状态和扣减库存是实时且同步的操作,但通知支付服务和物流服务则可以异步进行。使用MQ可以帮助我们实现以下目标:

    • 解耦服务:订单服务不需要直接调用支付服务和物流服务,降低了服务间的耦合度。
    • 异步处理:订单服务可以立即响应客户端,不必等待支付和物流服务的处理结果。
    • 流量削峰:在高峰期,MQ可以缓冲大量的订单处理请求,避免服务被压垮。

订单服务生产消息: 当订单服务完成订单状态更新和库存扣减后,它将以下消息发送到

{
  "orderId": "123456789",
  "status": "paid",
  "userId": "user123",
  "items": [
    {"productId": "prod123", "quantity": 1},
    {"productId": "prod456", "quantity": 2}
  ]
}

这个消息将被发送到不同的主题或队列,例如payment_topiclogistics_topic

2. 支付服务和物流服务消费消息:

  • 支付服务订阅payment_topic,当接收到订单支付消息后,它会处理支付逻辑,如验证支付状态、记录交易日志等。
  • 物流服务订阅logistics_topic,当接收到订单消息后,它会准备发货,更新物流信息,并通知用户。

通过这种方式,订单服务可以快速响应用户请求,而支付和物流服务可以按照自己的节奏处理订单相关的操作,整个系统因此变得更加灵活和可扩展。

如何保证消息不丢失?

保证生产者能够成功发送到交换机和队列(存储消息),生产者提供了消息确认机制
到队列后消息要有持久化机制
消费者要有一个消息确认机制 保证消费者至少消费成功消息一次

  • 开启生产者确认机制,确保生产者的消息能到达队列
    confirm到交换机ack 不到nack 和 return没到返回nack机制保证生产者把消息发过去

  • 开启持久化功能,确保消息未消费前在队列中不会丢失
    万一broker挂掉就惨了 保证至少成功一次消费

  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
    消费者三种机制:

    RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息,而SpringAMQP则允许配置三种确认模式:

    • manual:手动ack,需要在业务代码结束后,调用api发送ack。

    • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

    • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

  • 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

    在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

  • 异步发送(验证码、短信、邮件)
  • MySQL和Redis,ES之间的数据同步
  • 分布式事务
  • 削峰填谷

如何解决消息积压?

产生原因:当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是堆积问题

**解决消息堆积有三种思路 **

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限

如何保证消费幂等性【MQ】

幂等性是指同一个操作执行多次和执行一次的效果相同。在消息消费的场景中,保证幂等性通常有以下几种方法:
利用数据库的唯一约束
在数据库中为消息设置唯一标识(如消息ID),在处理消息前先检查该标识是否已存在。
导致重复消费 返回ack,blocker未收到。一定要在
生产者
投递的时候生成全局唯一的id,消费者就会去判断。异步生成 拿订单号去数据库查 如果查得到就直接return
精髓就是全局唯一
UUID不行 因为每次发送的消息都不是同一个UUID 要用业务上的

哪些地方还会有幂等问题?

提供者的openfegin、xxl-job、被别人调用且涉及到增删改

状态记录:

在消费消息前,记录消息的状态(如已处理),处理完毕后再更新状态。要根据订单ID+状态 来保证消费幂等性。订单存在且未支付 → 更新锁订单ID

并发情况幂等性:

完美的幂等要加上分布式锁对敏感性要求高,且要控制好锁的力度

如何保证消费有序性

队列中可以指定消息的消费顺序

RabbitMQ → 多个生产者并发投,所以生产者不能保证有序性,只考虑消费有序性。以消息进入的MQ的消息去回答。

怎么保证?
单线程消费:

在消费者端使用单个线程处理消息,确保消息按顺序处理。

分区有序:

在如Kafka这样的消息队列中,可以保证同一个分区内的消息是有序的。

如何既要又要【有序 + 速度快】

既要又要”通常指的是在保证消息的幂等性和有序性的同时,还需要考虑其他特性(如高性能、高可用等)
在一些场景下,可能需要在性能和一致性之间做权衡。例如,可以选择最终一致性来换取更高的性能。
Kafka 和 RocketMQ可以天生实现【底层Hash取模】

若非要用RabbitMQ实现呢?

不同订单之间是否要求一定顺序??
镜像集群,先搭3个节点的镜像集群,建立三个队列分为不同的镜像节点 各占一个队列,需要自己去实现
对订单号进行hash取模看到落到哪个节点
三个队列至少三个消费者 分别去消费它们
此时就可以并行有三个消费者去执行
把业务数据没关系的放在不同的队列去管理

万一挂掉了呢?

队列有持久化不用担心

能说一下如何使用死信交换机吗支付

死信交换机(DLX)用于处理无法正常消费的消息
创建一个正常的交换机和队列,以及一个死信交换机和死信队列。

  1. 定义死信交换机:创建一个用于处理死信的交换机。
  2. 定义死信队列:创建一个队列用于接收死信,并将其绑定到死信交换机。
  3. 配置主队列的死信交换机属性:在主队列上设置参数,指定当消息无法正常处理时应该发送到哪个死信交换机。
  4. 发送消息到主队列:生产者将消息发送到主交换机,进而路由到主队列。
  5. 消费主队列消息:消费者从主队列中获取消息并进行处理。如果消息处理失败,它将被路由到死信交换机。
  6. 消费死信队列消息:设置消费者来处理死信队列中的消息,进行错误处理或记录日志等操作。

mysql如何提升深分页查询效率子查询+索引

使用索引

  • 确保查询中使用的列上有适当的索引,这样可以加快查找速度。

**避免使用OFFSETLIMIT**:

  • 使用OFFSET进行深分页时,MySQL需要遍历所有OFFSET之前的行。可以通过记住上一次查询的最大ID来避免使用OFFSET

使用条件过滤

  • 如果可能,使用WHERE子句来减少需要扫描的数据量。

增加LIMIT的大小

  • 如果业务允许,可以增加每次查询返回的结果集大小,减少分页次数。

缓存

  • 对于不经常变更的数据,可以使用缓存来存储已经查询过的页。

使用EXPLAIN分析查询

  • 使用EXPLAIN来分析查询计划,找出性能瓶颈并进行优化。

能说一下常用的存储引擎以及它们的差异吗

InnoDB
支持事务、行级锁和外键。
适合处理大量短期事务。
为了维护数据的完整性,写操作相对较慢

MyISAM

不支持事务、不支持行锁只支持表锁
并发没那么大 事务要求没那么高可以用

能说一下倒排索引的原理吗?

根据参与文档中的字段 要构建倒排就会去分词
根据用户索引也会分词 就会去查文档id 再去查文档
中文词库为IK (Ikun 你干嘛 哎哟~)

es的text和keyword的区别

text

用于全文搜索,会分词,字符串类型

keywod

用于精确搜索字段,不会被分词,字符串类型

es在你的项目中是用来做什么的

快速搜索商品(C端)、订单(后台) + 日志查询 + 地理位置搜索经纬度定位附近的事物

mysql和ElasticSearch如何做数据同步

mysql进行增删改的时候
对数据敏感性实时性要求没那么高 只看可靠性[MQ异步 + 定时任务 = 没有那么强一致性]

如果数据量没那么大 有没有必要上ES?

没有必要,正排索引不走全表扫描也蛮快
组长进行技术选型 考虑到以后的业务增长

项目已经上线了 但是中途想换成ES 怎么办

mysql是全量数据 mq只能同步增量数据 怎么办呢?
新上架的只能到ES 那应该如何?

此时涉及到全量和增量的同步与Redis不一样
加定时任务每周/每天 会定期重构一次索引库晚上跑→兜底模式,全量同步,后期再增量同步

能说一下分词的原理吗

底层是大数据量的内容 树的结构来构建分词 IK,字符分割、词汇识别、过滤停用词
不好意思面试官 具体底层原理不是很了解

使用ES有遇到什么问题吗

类似于深分页!
测试环境数据量不会很大 等到上线后才会有这种问题

说一下jvm的内存区域,以及每个区域是干什么的

虚拟机栈、本地方法栈、程序计数器、元空间、堆

运行时数据区

虚拟机栈:每个线程运行时所需要的内存(先进后出)方法调用过程。每个栈由多个栈帧组成,对应着每次方法调用所占用的内存。每个线程只能有一个活动栈帧,对应着当前执行的那个方法

本地方法栈:与虚拟机栈类似,区别→虚拟机栈执行java方法,本地方法栈执行native方法【被封装的方法 没有具体实现的 都封装在java虚拟机中】专门存储java写的局部方法的局部变量

程序计数器:是当前线程所执行的字节码指令的行号指示器 同一个核是错峰出行 会上下文切换,要用程序计数器记录下当前执行到哪里的代码

元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。最大区别是元空间在本地内存中而不是虚拟机中。1.8以前叫永久代 1.8后叫方法区或元空间 一般存储类元信息。还会存有运行时常量池。在里面还会存有静态变量

堆内存:是JVM所有线程共享的部分,唯一用途是来保存对象实例、数组;由年轻代和老年代组成。new一个都会开启一个空间

类加载器:类加载器(Class Loader)负责将类的字节码文件(.class文件)加载到JVM中,并转化为对应的java.lang.Class对象,以供Java程序使用。

执行引擎:执行引擎(Execution Engine)是负责执行字节码的核心组件。执行引擎的作用是读取字节码指令,对它们进行解析并执行,从而实现Java程序的功能

本地库接口:是Java虚拟机的一部分,它允许Java程序调用其他语言编写的本地应用程序和库(通常是C或C++)。这是因为Java本身设计为平台无关的语言,但它有时需要与特定平台的底层系统或硬件进行交互,而这通常是通过本地代码实现的。

直接内存(Direct Memory):直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

说一下常见的垃圾回收算法 他们的特点是什么?

标记清除算法:将垃圾回收分为2个阶段,标记和清除。内存碎片高,效率高,清除快
标记整理算法:一般用于老年代,将存活的对象都向另一端移动,然后清理边界以外的垃圾。[对象要移动,效率低],效率慢一些 需要把内存碎片移动,内存碎片没有 但是效率低一点
复制算法:将原有的内存空间一分为二,每次只用其中一块,正在使用的对象复制到另一个内存中,然后将该内存空间清空,交换两个内存的角色,完成垃圾回收。

被标记的是没有被回收的

说一下常见的垃圾回收器以及他们的特点垃圾收集器是垃圾收集算法的具体实现

串行垃圾收集器[新生代区]:指使用单线程进行垃圾回收(用户请求不能访问STW),堆内存较小适合个人电脑。底层用的复制算法
并行垃圾收集器:JDK8默认使用此垃圾回收器,在垃圾回收时,多个线程在工作(用户请求不能访问STW),并且java应用中所有线程都要暂停,等待垃圾回收的完成。底层用的复制算法
CMS(并发[Concurrent Mark Sweep])垃圾收集器:是一款并发的、使用标记—清除算法的垃圾回收器(针对老年代) 初始标记 → 并发标记 → 并发预清理 → 最终标记 → 并发清除 → 并发重置 小于8G内存用CMS 一般用并发垃圾收集器配合收集年轻小内存
G1垃圾收集器[复制算法]:Eden(2M)、最大回收停顿时间、大内存适合用G1、大于8G内存用G1

说一下cms的各个阶段过程以及特点CMS一般都清理老年代

过程:初始标记 → 并发标记 → 并发预清理 → 最终标记 → 并发清除 → 并发重置
是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
其最大特点是在进行垃圾回收时,应用仍然能正常运行

初始标记:找GCRoot根去的第一层对象[STW] 速度是非常快的。为什么STW?如果找的时候同时也要标记 那么若这时候有的未被标记那就有大BUG。
并发标记:从第一层找到后的之后那些对象不能被回收(CMS回收器会遍历老年代,标记出所有活动的对象),在这个阶段,应用程序线程与垃圾回收线程并发运行[能接收用户请求],会产生新的对象。
并发预清理:这个阶段也是并发执行的,目的是处理在并发标记阶段应用程序线程产生的新垃圾,CMS回收器会清理那些在并发标记阶段被修改的对象,并执行一些预清理工作,以减少下一个STW阶段的暂停时间。
最终标记:这个阶段是STW的,它是为了处理在并发标记和并发预清理阶段未被处理的对象。CMS回收器会完成所有剩余的标记工作,确保所有存活的对象都被正确标记
并发清除:在这个阶段,应用程序线程与垃圾回收线程再次并发运行。CMS回收器会清除未被标记的对象,释放内存空间。清除过程中不会移动存活对象,因此可能会产生内存碎片。
并发重置:这个阶段是并发执行的,目的是重置CMS数据结构,为下一次垃圾回收做准备。

说一下g1的各个阶段过程以及特点,

划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备
分成三个阶段:新生代回收(STW)、并发标记(重新标记STW)、混合收集。如果并发失败(即回收速度赶不上创建新对象速度),就会触发Full GC

你们项目是用哪一个垃圾收集器,为什么用这个?

每个服务两个节点 8G4核 用CMS 太大就用G1

什么样的对象会被成为垃圾对象?

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收

怎么确定什么是垃圾?
  • 引用计数法
    一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

  • 可达性分析算法

    1. 标记阶段:从GCRoots开始,标记所有可达的对象。
    2. 回收阶段:遍历堆中的所有对象,回收那些未被标记的对象所占用的空间。

    采用的都是通过可达性分析算法来确定哪些内容是垃圾

    通过可达性分析算法,那些从任何GCRoots都无法到达的对象被认为是不可达的,因此可以被垃圾回收器回收。这种方法能够处理循环引用的情况,这是引用计数法所无法解决的

静态的成员变量[元空间 基本不会被回收] 局部变量 成员属性

说一下双亲委派机制以及优点

加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
优点:

  • 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
  • 为了安全,保证类库API不会被修改

用相同的加载器在同一个路径下只能用同一个路径的类 用不同的加载器就可以加载

如何打破双亲委派模型?

自定义类加载器

  • 通过自定义类加载器,并重写loadClass方法,可以实现在加载类时不遵循双亲委派模型。在自定义类加载器中,你可以直接尝试加载类,而不是先委派给父类加载器。

    public class CustomClassLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            // 直接加载,不委派给父类加载器
            return findClass(name);
        }
    }
    

jvm常用的参数有哪些?代表什么意思

jps 进程状态信息
jstack 查看进程内线程的堆栈信息(产生死锁也可以查看)
jmap 查看堆栈信息(生成堆栈内存快照,内存使用信息)
jhat 堆转存快照分析工具
jstat JVM统计监测工具

  1. -Xms-Xmx
    • -Xms:设置JVM堆内存的初始大小(最小值)。
    • -Xmx:设置JVM堆内存的最大大小。
  2. -Xss
    • 设置每个线程的堆栈大小。
  3. -XX:NewSize-XX:MaxNewSize
    • -XX:NewSize:设置新生代内存的初始大小。
    • -XX:MaxNewSize:设置新生代内存的最大大小。
  4. -XX:SurvivorRatio
    • 设置新生代中Eden区与Survivor区的容量比例。
  5. -XX:PermSize-XX:MaxPermSize(Java 8及之前版本):
    • -XX:PermSize:设置永久代(方法区)的初始大小。
    • -XX:MaxPermSize:设置永久代的最大大小。
  6. 设置元空间大小
    1. -XX:MetaspaceSize
      • 设置元空间的初始大小。当元空间耗尽时,JVM会尝试扩展元空间的大小,直到达到最大值(如果设置了的话)。
    2. -XX:MaxMetaspaceSize
      • 设置元空间的最大大小。如果没有设置这个参数,元空间的大小只受本地内存限制。

四大引用分别是什么,代表什么意思?

强引用:只要所有 GC Roots 能找到,就不会被回收。
软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够时候会回收软引用对象
弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把引用对象回收
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存

● 强引用指的就是代码中普遍存在的赋值方式,比如A 但是不a = new A()这种。强引用关联的对象,永远不会被GC回收。
● 软引用可以用SoftReference来描述,指的是那些有用是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
● 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,只要进行了垃圾回收,就会把引用对象回收。
● 虚引用他必须和ReferenceQueue一起使用,必须配合引用队列使用,被引用对象回收时,
会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存。

什么是内存溢出?什么是内存泄露?

内存溢出:内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,导致所需要的内存超出了系统所能提供的最大内存。【会先进行一次GC实在不够就OOM 堆\栈(递归过多,局部变量过多)\方法区都有可能发生内存溢出】
内存泄露ThreadLocal会导致(未及时释放):内存泄露是指程序中已分配的内存由于某种原因未能释放,即使在不再需要这些内存的情况下,它们仍然保持分配状态,导致可用内存逐渐减少

你的项目中出现过内存泄漏吗?你是怎么排查并且解决的?

有的,内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况
1、通过jmap或设置jvm参数获取堆内存快照dump
2、通过工具,VisualVM去分析dump文件,VisualVM可以加载离线的dump文件
3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
4、找到对应的代码,通过阅读上下文的情况,进行修复即可

异常的祖先是:Throwable

怎么解决cpu飙高

使用top命令查看占用cpu的情况
通过top命令查看后,可以查看是哪一个进程占用cpu较高
使用ps命令查看进程中的线程信息
使用jstack命令查看进程中哪些线程出现了问题,最终定位问题

  • 使用top命令查看占用cpu的情况哪个进程占用的cpu最高

    finalShell中输入 top

  • 查看进程中的线程信息 ps H -eo pid,tid,%cpu | gerp pid

  • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    因为是十六进程所以要十进程转换十六进程
    直接linux输入 printf "%x\n" Pid
    然后就可以根据十六进制的去找哪个线程cpu占用
    之后查看文件是cat xxx

项目中遇到的难点

1.从业务角度 曾经做过保险项目 金融保险领域 行外人需要花很多时间了解需求还有同事的帮助才懂得需求知道如何去写

阅读全文

JL

2025/2/7

招聘流程

主流网站:Boss直聘
先过系统筛选[写本科 写计算机专业 过筛选 得到面试机会]
HR会帮我们的 需要KPI当月目标和业绩
黑马课程对标3年工作经验 大厂211都不好进

每次都要微信问一下HR:**技术面** 还是 人力面
可以打听面试官侧重点,问一下是领导还是主管还是技术组长

不要乱讲话!!不要乱讲话!!不要乱讲话!!不要乱讲话!!

学信网可查吗? 先说可查 若让 发编号 就下一家 [可填专科吗???]


简历怎么写好?

基本信息 籍贯 工作年限 求职岗位(Java开发工程师) 薪资(面议)
薪资范围6-18k 拉大范围 入职时间:一周内到岗
电话必写 邮箱也必写

面试问你什么时候能到岗3个工作日左右 / (3天左右)

image-20250207105915015

教育经历
25届 19-23 也可以大三出去实习 21-24
在校:往前推一年
学校 + 专业

工作经历
每一家一年多 建议2个公司
上一家公司距离现在 不能有很空窗时期【实时修改工作经验时间

上周离职
有没有面试其他?
没有 我也是刚好碰到贵公司 有 拿到了offer但是我还是意愿最大贵公司喜欢贵公司的环境

开发技能 [15条以上]
细节引导面试官去书写开发技能
记得要设置陷阱
从优先级高到低写
前端一定要写

Java基础先写到前面
大数据统计至少要 3年经验 4个项目经验

项目周期:时间控制在5-8个月
第一个项目经历至少7-8点 需要业务+技术点
第二个项目至少5-6条 达到项目一的七成
第三四项目至少3条 减量不减质【不要龙头蛇尾】

2025.2.10号中午收集好简历

文件名称:**Java开发工程师 _ 3年 _ 潘春尧**.pdf



求职意向

期望城市:不限
期望岗位:Java开发工程师
期望薪资:面议
到岗时间:一周内/随时
写虚岁!!

教育经历

民教网3年:2018.9-2022.7 xxx大学 专业:不相关可以不写

学信网
25届:实习
23-24届:1-2年

工作经历(一家公司超过两年左右) 5-8个月 / 一个项目

xxx公司(找自己熟悉城市,不要找在线招聘会被HR直接问对面招聘中的HR)

没有社保:不写期望城市
社保:写社保所在地
公司开业时间、小公司(100人以内)、科技有限公司、上班地点交通方式、背景调查写洪哥、法人(老板)、公司地址

背调:模板
写纸质版信息表 出生年月日 → 写真实的日期

第一步:写纸质信息表(证明人:电话)洪哥敏姐
背调信息:发给洪哥敏姐

hr会根据你给的电话直接
上级领导:项目
第三者:朋友/同学/同事 [可以拒绝回答很多]

第三方背调:查社保 查工作记录 查学信网

带身份证去的话 证明楼下的大厦需要登记身份信息才能进



Boss上的投递

上传PDF版本的简历
工作经历 → 对该公司隐藏我的简历

文件名称:**潘春尧-本科-3年-Java开发工程师**.pdf

尽量先从远的地方投递简历 试试状态!最想去广州的留到最后状态好的时候再投递

外包公司:跟培训的学生匹配 哈哈哈!



五险一金:基本养老保险[工作里面累计交满20年退休时领退休金] 、 基本医疗保险 [总部上海现在在广州一般都是本地医保方便]、 失业保险[工作满一年后因公司被辞退才可以领取] 、 工伤保险 、 生育保险 及 住房公积金

【要找公积金的公司】公积金5% → 买房可以用公积金来贷款;退休后这笔钱可以一次性提取出来养老;装修/租房的名义提取(半年一次)

面试题问:上家公司的工资待遇 → 城市最低标准买的
10k扣除五险一金 保险400-500 公积金大概300-400左右

个人所得税:不超过3.6w就是3%,每个月财务都会扣除发放税后工资,起征点5k,超过5k的部分才会交税。10k以内的大概是3%,

面试:公司有绩效考核 我们没有很明确的考核 日常都能完成工作量

入职当天要签劳务合同 1年或3年?
区别是什么?
1年的劳动合同只能定1个月试用期 2年2个月 3年3个月
大概率试用期8-9折 都是3-6个月

签订劳动合同可以离职 末尾淘汰在法律上不允许
转正以后被裁可以n+1个月 n代表年 试用期被辞退会赔偿半个月薪资

民教网(1000人以下的公司) 主攻针对小型企业 和 二三线城市
浙江→绍兴杭州
远公司约线上公司

现在在外地 能不能开一轮的线上面试 如果有需求我还是能过去的
阅读全文

ElasticSearch

2025/1/3

ElasticSearch结合Kibana、Logstash、Beats,核心是elastic stack的核心,负责存储、搜索、分析数据
Lucene的优势:容易扩展、高性能(基于倒排索引)
Lucene的缺点:直选与java语言开发
ElasticSearch是基于Lucene开发的

Elasticsearch:开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能

正向索引和倒排索引

传统数据库是正向索引

ElasticSearch采用倒排索引

  • 文档(document):每条数据就是一个文档
  • 词条(term):文档按照语义分成词语
id title price
1 小米手机 3499
2 华为手机 4999
3 华为小米充电器 49
4 小米手环 299

↓↓↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓

词条(term) 文档id
小米 1,3,4
手机 1,2
华为 2,3
充电器 3
手环 4

过程:搜索华为手机 → 得到:华为手机两个词条 → 得到每个词条所在文档id:华为:2,3 手机:1,2 → 得到id为1,2,3的文档 → 存入结果集
倒排索引:对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时现根据词条查询到文档id,而后获取到文档
正排索引:基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条

文档

ElasticSearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息,文档数据会被序列化为json格式后存储在ElasticSearch中
在ElasticSearch中

  • **索引(index)**:相同类型的文档的集合
  • **映射(mapping)**:索引中文档的字段约束信息,类似表的结构约束
概念对比
MySQL ElasticSearch 说明
Table Index 索引(index),就是文档的集合,类似于数据库的表(table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
Column Field 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射),就是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQL DSL DSL是ElasticSearch提供的JSON风格的请求语句,用来操作ElasticSearch,实现CRUD
架构

MySQL写操作:擅长事务类型操作,可以确保数据的安全和一致性
ElasticSearch查询:擅长海量数据的搜索、分析、计算
[可以互补达到数据双写一致性]

1.1.创建网络

因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络(创建过的不用再创建):

docker network create es-net

1.2.加载镜像

这里我们采用elasticsearch的7.12.1版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己pull。

课前资料提供了镜像的tar包:

image-20210510165308064

大家将其上传到虚拟机中,然后运行命令加载即可:

# 导入数据
docker load -i es.tar

同理还有kibana的tar包也需要这样做。

1.3.运行

运行docker命令,部署单点es:

docker run -d \
    --name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1

命令解释:

  • -e "cluster.name=es-docker-cluster":设置集群名称
  • -e "http.host=0.0.0.0":监听的地址,可以外网访问
  • -e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小
  • -e "discovery.type=single-node":非集群模式
  • -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录
  • -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录
  • -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录
  • --privileged:授予逻辑卷访问权
  • --network es-net :加入一个名为es-net的网络中
  • -p 9200:9200:端口映射配置

在浏览器中输入:http://192.168.xxx.xxx:9200 即可看到elasticsearch的响应结果:

image-20210506101053676

2.部署kibana

kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。

2.1.部署

运行docker命令,部署kibana,同理先加载镜像: docker load -i kibana.tar,然后启动:

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1
  • --network es-net :加入一个名为es-net的网络中,与elasticsearch在同一个网络中
  • -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
  • -p 5601:5601:端口映射配置

kibana启动一般比较慢,需要多等待一会,可以通过命令:

docker logs -f kibana

查看运行日志,当查看到下面的日志,说明成功:

image-20210109105135812

此时,在浏览器输入地址访问:http://192.168.xxx.xxx:5601,即可看到结果

分词效果概览 Dev Tools - Elastic

GET /_analyze
{
“analyzer”: “standard”,
“text”: “黑马程序员”
}

{
  "tokens" : [
    {
      "token" : "黑",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "马",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "程",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<IDEOGRAPHIC>",
      "position" : 2
    },
    {
      "token" : "序",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    },
    {
      "token" : "员",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "<IDEOGRAPHIC>",
      "position" : 4
    }
  ]
}

如果是分析中午就不能用它原有的,存在明显的问题:将中文逐字分词,没有任何业务语义,因此需要借助专业的分词器

3.安装IK分词器

3.1.在线安装ik插件(较慢)

# 进入容器内部
docker exec -it elasticsearch /bin/bash

# 在线下载并安装
./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip

#退出
exit
#重启容器
docker restart elasticsearch

3.2.离线安装ik插件(推荐)

1)查看数据卷目录

安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:

docker volume inspect es-plugins

显示结果:

[
    {
        "CreatedAt": "2022-05-06T10:06:34+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
        "Name": "es-plugins",
        "Options": null,
        "Scope": "local"
    }
]

说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data 这个目录中。

2)解压缩分词器安装包

下面我们需要把课前资料中的ik分词器解压缩,重命名为ik

image-20210506110249144

3)上传到es容器的插件数据卷中

也就是/var/lib/docker/volumes/es-plugins/_data

image-20210506110704293

4)重启容器

# 4、重启容器
docker restart es
# 查看es日志
docker logs -f es

5)测试:

IK分词器包含两种模式

  • ik_smart:最少切分

  • ik_max_word:最细切分

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "黑马程序员学习java太棒了"
}

结果:

{
  "tokens" : [
    {
      "token" : "黑马",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "程序员",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "程序",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "员",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "CN_CHAR",
      "position" : 3
    },
    {
      "token" : "学习",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "java",
      "start_offset" : 7,
      "end_offset" : 11,
      "type" : "ENGLISH",
      "position" : 5
    },
    {
      "token" : "太棒了",
      "start_offset" : 11,
      "end_offset" : 14,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "太棒",
      "start_offset" : 11,
      "end_offset" : 13,
      "type" : "CN_WORD",
      "position" : 7
    },
    {
      "token" : "了",
      "start_offset" : 13,
      "end_offset" : 14,
      "type" : "CN_CHAR",
      "position" : 8
    }
  ]
}

3.3 扩展词词典

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑,可以直接linux系统vi编辑

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等。

所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。

1)打开IK分词器config目录:/var/lib/docker/volumes/es-plugins/_data/ik/config/IKAnalyzer.cfg.xml

image-20210506112225508

2)在IKAnalyzer.cfg.xml配置文件内容添加:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
      
        <entry key="ext_dict">ext.dic</entry> 
</properties>

3)新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改

传智播客
奥力给

4)重启elasticsearch

docker restart es

# 查看 日志
docker logs -f es

image-20201115230900504

日志中已经成功加载ext.dic配置文件

5)测试效果:

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "传智播客Java就业超过90%,奥力给!"
}

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑,可以直接linux系统vi编辑

3.4 停用词词典

在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。

IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。

1)IKAnalyzer.cfg.xml配置文件内容添加:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典-->
        <entry key="ext_dict">ext.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典  *** 添加停用词词典-->
        <entry key="ext_stopwords">stopword.dic</entry>
</properties>

3)在 stopword.dic 添加停用词

习大大

4)重启elasticsearch

# 重启服务
docker restart es
docker restart kibana

# 查看 日志
docker logs -f es

日志中已经成功加载stopword.dic配置文件

5)测试效果:

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "传智播客Java就业率超过95%,习大大都点赞,奥力给!"
}

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑



索引库操作

mapping属性
mapping是对索引库中文档的约束,常见的mapping属性包括:

• type:字段数据类型,常见的简单类型有:
• 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
• 数值:long、integer、short、byte、double、float
• 布尔:boolean
• 日期:date
• 对象:object
• index:是否创建索引,默认为true【默认倒排】
• analyzer:使用哪种分词器【只有text才需要分词】
• properties:该字段的子字段

{
    "age": 21,
    "weight": 52.1,
    "isMarried": false,
    "info": "黑马程序员Java讲师",
    "email": "zy@itcast.cn",
    "score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}
索引库操作
创建索引库和mapping的请求语法
PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

# 创建索引库
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "email":{
        "type": "keyword",
        "index": false
      },
      "name":{
        "type": "object",
        "properties": {
          "firstName":{
            "type": "keyword"
          },
          "lastName":{
            "type": "keyword"
          }
        }
      }
    }
  }
}
---------------------------------------------------------------------
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "heima"
}

操作索引库禁止修改索引库(因为已经映射好了)

查看索引语法:

GET/索引名

删除索引库的语法:

DELETE/索引库名

可以在修改索引的过程中添加新的字段
PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}
---------------------------
PUT /heima/_mapping
{
  "properties": {
    "age":{
      "type": "integer"
    }
  }
}

当您使用Elasticsearch(ES)的PUT请求创建索引库时,这个索引库实际上是存储在Elasticsearch集群的节点上的。如果您的Elasticsearch集群是安装在Linux服务器上的,那么是的,索引库会被创建在Linux文件系统中。

Elasticsearch为每个索引分配一个或多个主分片,并为每个主分片分配一个或多个副本分片。这些分片实际上是存储在Elasticsearch节点的文件系统上的。具体来说,索引数据存储在以下路径:

复制

/path/to/elasticsearch/data/nodes/<node-id>/<index>/<shard-id>

这里的/path/to/elasticsearch是Elasticsearch的安装路径,data目录是默认的数据存储位置,nodes目录包含了集群中各个节点的数据,<node-id>是节点的唯一标识,<index>是您创建的索引名称,而<shard-id>则是分片的ID。

索引库的增删改查汇总


# 创建索引库
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "email":{
        "type": "keyword",
        "index": false
      },
      "name":{
        "type": "object",
        "properties": {
          "firstName":{
            "type": "keyword"
          },
          "lastName":{
            "type": "keyword"
          }
        }
      }
    }
  }
}

# 查询
GET /heima

# 修改索引库
PUT /heima/_mapping
{
  "properties":{
    "age":{
      "type": "integer"
    }
  }
}

# 修改
DELETE /heima
文档操作——添加文档
# 每次写操作的时候 版本会增加 "_version ++"

# 插入文档
POST /heima/_doc/1
{
  "info": "广州黑马198班",
  "email": "390415049@qq.com",
  "name":{
    "firstName": "春",
    "lastName": "尧"
  }
}

# 查询文档
GET /heima/_doc/1

# 删除文档
DELETE /heima/_doc/1
文档操作——修改文档
方式一:全量修改,会删除旧文档,添加新文档
PUT /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}
-------------------------
PUT /heima/_doc/1
{
    "info": "黑马程序员高级Java讲师",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}


# 全量修改文档
POST /heima/_doc/1
{
  "info": "广州黑马198班",
  "email": "90415049@qq.com",
  "name":{
    "firstName": "尧",
    "lastName": "春"
  }
}
方式二:增量修改,修改指定字段值
POST /索引库名/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}
-------------------------
POST /heima/_update/1
{
  "doc": {
    "email": "ZhaoYun@itcast.cn"
  }
}


# 局部修改文档
POST /heima/_update/1
{
  "doc": {
    "email": "ZYun@itcast.cn"
  }
}
总结

文档操作有哪些?

  • 创建文档:POST /索引库名/_doc/文档id { json文档 }

  • 查询文档:GET /索引库名/_doc/文档id

  • 删除文档:DELETE /索引库名/_doc/文档id

  • 修改文档:

    • 全量修改:PUT /索引库名/_doc/文档id { json文档 }

    • 增量修改:POST /索引库名/_update/文档id { “doc”: {字段}}

JavaRestClient

Elasticsearch目前最新版本是8.0,其Java客户端有很大变化。不过大多数企业使用的还是8以下版本,所以我们选择使用早期的JavaRestClient客户端来学习。官方文档地址:Elasticsearch Clients | Elastic

阅读全文

MyBatisPlus

2024/11/23

MyBatis-Plus
简介 | MyBatis-Plus

引入MybatisPlus起步依赖写依赖+认爸爸
  • MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter:
<!--MybatisPlus-->
<dependency>    
    <groupId>com.baomidou</groupId>    
    <artifactId>mybatis-plus-boot-starter</artifactId> 
    <version>3.5.3.1</version>
</dependency>
  • 自定义的Mapper继承MybatisPlus提供的BaseMapper接口
public interface UserMapper extends BaseMapper<User> {
}
UserMapper.java

public interface UserMapper extends BaseMapper<User> {
UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper">

</mapper>

常用注解

  • @TableName:用来指定表名
  • @Tableld:用来指定表中的主键字段信息
    • IdType枚举
      • AUTO:数据库自增长
      • INPUT:通过set方法自行输入
      • ASSIGN_ID分配ID 默认实现类是雪花算法
  • @TableField:用来指定表中的普通字段信息默认驼峰转下划线,不一致需要改
    • 成员变量名与数据库字段名不一致
    • 成员变量名是以is开头,且是布尔值isMarried
    • 成员变量名与数据库关键字冲突order
    • 成员变量不是数据库字段address,要标记不存在不然会默认数据库字段
@Data
public class User {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    @TableField("username")
    private String name;

    @TableField("is_married")
    private Boolean isMarried;  // is经过反射会默认变成变量名Married
    
    @TableField("`order`")
    private Integer order;
    
    @TableField(exist = false)
    private String address;
......
}

此时如果数据库的表名是:tb_user 就需要用到 @TableName

数据库名:tb_user (用户表)
#  名称
1  id
2  username
3  is_married
4  order

常见配置

MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置
MP更擅长单表的增删改查,如果是多表还是推荐用xml

mybatis:
  mapper-locations: classpath*:mapper/*.xml # Mapper.xml文件地址,默认值
  type-aliases-package: com.itheima.po # 别名扫描包
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名自动映射
    cache-enabled: false # 是否开启二级缓存
  global-config:
    db-config:
      id-type: assign_id # id为雪花算法生成
      update-strategy: not_null # 更新策略:只更新非空字段 类似于动态sql

MyBatisPlus使用的基本流程

  • 引入起步依赖
  • 自定义Mapper基础BaseMapper
  • 在实体类上添加注释声明 表信息
  • 在application.yml中根据需要添加配置

核心功能—条件构造器

条件构造器

MyBatisPlus支持各种复杂的where条件,满足日常开发的所有需求

  • 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
# 原始SQL:
SELECT id,username,info,balance
FROM user
WHERE username LIKE ? AND balance >= ?
// MyBatisPlus:
@Test
    void testQueryWrapper(){
        // 1.构建查询条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>()
                .select("id", "username", "phone")
                .like("username", "o")
                .ge("balance", 1000);
        // 2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }
// MyBatisPlus Lambda编码格式(解决硬编码):
 @Test
    void testLambdaQueryWrapper(){
        // 1.构建查询条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                // 利用反射 解决字符串硬编码
                .select(User::getId, User::getUsername, User::getPhone)
                .like(User::getUsername, "o")
                .ge(User::getBalance, 1000);
        // 2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

-------------------------------------------------------------------------
@Test
void testLambdaQueryWrapperWithQueryWrapper(){
    // 1. 构建查询条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
    LambdaQueryWrapper<User> wrapper = queryWrapper.lambda()
            .select(User::getId, User::getUsername, User::getPhone)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000);

    // 2. 查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}
  • 更新用户名为jack的用户的余额为2000
# 原始SQL:
UPDATE user
    SET balance = 2000
    WHERE (username = "jack")
// MyBatisPlus:
@Test
    void testUpdateByQueryWrapper(){
        // 1.要更新的数据
        User user = new User();
        user.setBalance(2000);
        // 2.更新的条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "jack");
        // 3.执行更新
        userMapper.update(user, wrapper);
    }
  • 更新id为1,2,4的用户的余额,扣200
# 原始SQL:
UPDATE user
    SET balance = balance - 200
    WHERE id in (1,2,4)
// MyBatisPlus:
@Test
    void testUpdateWrapper(){
        List<Long> ids = List.of(1L, 2L, 4L);
        UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
                .setSql("balance = balance - 100")
                .in("id", ids);
        userMapper.update(null, wrapper);
    }
条件构造器的用法:
  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分

  • UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用

  • 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码

4. 字段映射与表名映射

4.1 问题一:表字段与编码属性设计不同步

  • 在模型类属性上方,使用**@TableField**属性注解,通过==value==属性,设置当前属性对应的数据库表中的字段关系。

1683796001750

4.2 问题二:编码中添加了数据库中未定义的属性

  • 在模型类属性上方,使用**@TableField注解,通过==exist==**属性,设置属性在数据库表字段中是否存在,默认为true。

1683796121907

4.3 问题三:表名与编码开发设计不同步

  • 模型类上方,使用**@TableName注解,通过==value==**属性,设置当前类对应的数据库表名称。

1683798660359

四、主键生成策略

id主键生成的策略有哪几种方式?

不同的表应用不同的id生成策略

  • 日志:自增(1,2,3,4,……)
  • 购物订单:特殊规则(FQ23948AK3843)
  • 外卖单:关联地区日期等信息(10 04 20200314 34 91)
  • 关系表:可省略id
  • ……

1 id生成策略控制(@TableId注解)

雪花算法:@TableId(type= IdType.ASSIGN_ID)
ASSIGN_UUID是趋势递增
用了分库分表就不能用默认的id自增了 要用雪花算法

  • 名称:@TableId

  • 类型:属性注解

  • 位置:模型类中用于表示主键的属性定义上方

  • 作用:设置当前类中主键属性的生成策略

  • 相关属性

    type:设置主键属性的生成策略,值参照IdType枚举值

    image-20210801192449901

2 全局策略配置

mybatis-plus:
  global-config:
    db-config:
      id-type: assign_id #全局设置主键id策略
      table-prefix: tbl_  #表名前缀设置
id生成策略全局配置

image-20210801183128266

表名前缀全局配置

image-20210801183157694

自定义SQL

我们可以利用MyBatisPlus的Wrapper来**构造复杂的where条件**,然后自己定义SQL语句中剩下的部分。

将id在指定范围的用户(1,2,4)的余额扣减指定值
<update id = "updateBalanceByIds">
    UPDATE user
    SET balance = balance - #{amount}
    WHERE id IN
    <foreach collection="ids" separator="," item="id" open="(" close=")">
    #{id}
    </foreach>
</update>
  • 基于Wrapper构建where条件
// 1.更新条件
  List<Long> ids = List.of(1L, 2L, 4L);
  int amount = 200;
// 2.定义条件
  QueryWrapper<User> wrapper = new QueryWrapper<User>().in(User::getId, ids);
// 3.调用自定义SQL方法
  userMapper.updateBalanceByIds(wrapper, amount);
  • 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);
  • 自定义SQL,并使用Wrapper条件
<update id="updateBalanceByIds">
        update user
        set balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

IService接口基本用法

  • 自定义Service接口继承IService接口
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {

}
  • 自定义Service实现类,实现自定义接口并继承ServiceImpl类
package com.itheima.mp.service.impl.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.impl.IUserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

}
  • 搞了个测试类@Test
package com.itheima.mp.service.impl;

import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

@SpringBootTest
class IUserServiceTest {
    @Autowired
    private IUserService userService;

    @Test
    void testSaveUser() {
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userService.save(user);
    }
    @Test
    void testQuery(){
        List<User> users = userService.listByIds(Arrays.asList(1L, 2L, 3L));
        users.forEach(System.out::println);
    }

}

IService开发基础业务接口

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体
2 删除用户 DELETE /users/{id} 用户id
3 根据id查询用户 GET /users/{id} 用户id 用户VO
4 根据id批量查询 GET /users 用户id集合 用户VO集合
5 根据id扣减余额 PUT /users/{id}/deduction/{money} •用户id •扣减金额

解决在IDEA 的Maven下 出现 Cannot access in offline mode 问题 - Doyourself! - 博客园

管理接口文档

UserController.java
package com.itheima.mp.controller;

import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.po.User;

import com.itheima.mp.domain.dto.UserFormDTO;

import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Api(tags = "用户管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("users")
public class UserController {

    private final IUserService userService;

    @PostMapping
    @ApiOperation("新增用户")
    public void saveUser(@RequestBody UserFormDTO userFormDTO) {
        // 1.转换DTO为PO
        User user = BeanUtil.copyProperties(userFormDTO, User.class);
        // 2.新增
        userService.save(user);
    }

    @DeleteMapping("/{id}")
    @ApiOperation("删除用户")
    public void removeUserById(@PathVariable("id") Long userId) {
        userService.removeById(userId);
    }

    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@PathVariable("id") Long userId) {
        // 1.查询用户
        User user = userService.getById(userId);
        // 2.处理vo
        return BeanUtil.copyProperties(user, UserVO.class);
    }

    @GetMapping
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids) {
        // 1.查询用户
        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return BeanUtil.copyToList(users, UserVO.class);
    }

    @PutMapping("{id}/deduction/{money}")
    @ApiOperation("扣减用户余额")
    public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减的金额") @PathVariable("money") Integer money) {
        userService.deductBalance(id, money);
    }
}
UserFormDTO.java
package com.itheima.mp.domain.dto;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {

    @ApiModelProperty("id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("密码")
    private String password;

    @ApiModelProperty("注册手机号")
    private String phone;

    @ApiModelProperty("详细信息,JSON风格")
    private String info;

    @ApiModelProperty("账户余额")
    private Integer balance;
}
UserQuery.java
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
UserServiceImpl.java
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    public void deductBalance(Long id, Integer money) {
        // 1.查询用户
        User user = getById(id);
        // 2.判断用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }
        // 4.扣减余额
        baseMapper.deductMoneyById(id, money);
    }
}
package com.itheima.mp.mapper;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface UserMapper extends BaseMapper<User> {

    List<User> queryUserByIds(@Param("ids") List<Long> ids);

    void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);

    @Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
    void deductMoneyById(Long id, Integer money);
}

Iservice的Lambda方法

需求:实现一个根据复杂条件查询用户的接口,查询条件如下:

name:用户名关键字,可以为空
status:用户状态,可以为空
minBalance:最小余额,可以为空
maxBalance:最大余额,可以为空

<select id="queryUsers" resultType="com.itheima.mp.domain.po.User">
    SELECT *
    FROM tb_user
    <where>
        <if test="name != null">
            AND username LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="status != null">
            AND `status` = #{status}
        </if>
        <if test="minBalance != null and maxBalance != null">
            AND balance BETWEEN #{minBalance} AND #{maxBalance}
        </if>
    </where>
</select>
UserQuery.java
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
UserController.java
    @ApiOperation("根据复杂条件查询用户接口")
    @GetMapping("/list")
    public List<UserVO> queryUsers(UserQuery query) {
        // 1.查询用户PO
        List<User> users = userService.queryUsers(query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());
        // 2.把po拷贝到vo
        return BeanUtil.copyToList(users, UserVO.class);
    }
IUserService.java
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

import java.util.List;

public interface IUserService extends IService<User> {
    void deductBalance(Long id, Integer money);

    List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);
}
UserServiceImpl.java
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
   @Override
    public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) {
        return lambdaQuery()
                .like(name!=null, User::getUsername,name)
                .eq(status!=null, User::getStatus,status)
                .gt(minBalance!=null, User::getBalance,minBalance) // 大于
                .lt(maxBalance!=null, User::getBalance,maxBalance) // 小于
                .list();
    }
}

IService的Lambda更新LambdaUpdate()、LambdaQuery()

改造根据id修改用户余额的接口,要求如下
  • 完成对用户状态校验

  • 完成对用户余额校验

  • 如果扣减后余额为0,则将用户status修改为冻结状态 (2)

    UserController.java
 @PutMapping("{id}/deduction/{money}")
    @ApiOperation("扣减用户余额")
    public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减的金额") @PathVariable("money") Integer money) {
        userService.deductBalance(id, money);
    }
UserServiceImpl.java
 @Override
    public void deductBalance(Long id, Integer money) {
        // 1.查询用户
        User user = getById(id);
        // 2.判断用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }
        // 4.扣减余额 update tb_user set balance = balance - ?
        int remainBalance = user.getBalance() - money;
        lambdaUpdate()
                .set(User::getBalance, remainBalance)
                .set(remainBalance == 0,User::getStatus, 2)
                .eq(User::getId, id)
                .eq(User::getBalance, user.getBalance()) // 乐观锁
                .update();
    }

IService的批量新增

批量插入10万条用户数据,并作出对比:
  • 普通for循环插入4分钟
  • IService的批量插入30秒
  • 开启rewriteBatchedStatements=true参数【6秒】重写Statement语句,在application.yaml的sql中url拼接
Test  com/itheima/mp/service/IUserServiceTest.java
@Test
    void testSaveOneByOne() {
        long b = System.currentTimeMillis();
        for (int i = 1; i <= 100000; i++) {
            userService.save(buildUser(i));
        }
        long e = System.currentTimeMillis();
        System.out.println("耗时:" + (e - b));
    }

    private User buildUser(int i) {
        User user = new User();
        user.setUsername("user_" + i);
        user.setPassword("123");
        user.setPhone("" + (18688190000L + i));
        user.setBalance(2000);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(user.getCreateTime());
        return user;
    }
MyBatisPlus的批处理
@Test
void testSaveBatch() {
    // 准备10万条数据
    List<User> list = new ArrayList<>(1000);
    long b = System.currentTimeMillis();
    for (int i = 1; i <= 100000; i++) {
        list.add(buildUser(i));
        // 每1000条批量插入一次
        if (i % 1000 == 0) {
            userService.saveBatch(list);
            list.clear();
        }
    }
    long e = System.currentTimeMillis();
    System.out.println("耗时:" + (e - b));
}

可以看到使用了批处理以后,比逐条新增效率提高了10倍左右,性能还是不错的。

可以发现其实MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。SQL类似这样:

Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01

而如果想要得到最佳性能,最好是将多条SQL合并为一条,像这样:

INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES 
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);

该怎么做呢?

MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements。顾名思义,就是重写批处理的statement语句。参考文档:

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements

这个参数的默认值是false,我们需要修改连接参数,将其配置为true

修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: MySQL123

扩展功能 —— 代码生成器生成代码的代码

3.1 代码生成

在使用MybatisPlus以后,基础的MapperServicePO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成POMapperService等相关代码。只不过代码生成器同样要编码使用,也很麻烦。

这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。

3.1.1.安装插件

Idea的plugins市场中搜索并安装MyBatisPlus插件:

然后重启你的Idea即可使用。

3.1.2.使用

刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database

点击OK保存。

然后再次点击Idea顶部菜单中的other,然后选择Code Generator:

在弹出的表单中填写信息:

img

最终,代码自动生成到指定的位置了:

扩展功能 —— DB静态工具两个Service相互注入

  • 改造根据id查询用户的接口,查询用户的同时user表,查询出用户对应的所有地址address表
  • 改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
  • 实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常(练习)

3.2.静态工具

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

UserController.java
    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@PathVariable("id") Long id) {
        // 1.查询用户
//        User user = userService.getById(userId);
        // 2.处理vo
        return userService.queryUserAndAddressById(id);
    }
IUserService.java
public interface IUserService extends IService<User> {
    UserVO queryUserAndAddressById(Long id);
}
UserServiceImpl.java
@Override
    public UserVO queryUserAndAddressById(Long id) {
        // 1.查询用户
        User user = getById(id);
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 2.查询方法
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, id).list();
        // 3.封装VO
        // 3.1 转User的PO为VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        if (CollUtil.isEmpty(addresses)) {
           userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }

扩展功能—DB静态工具(练习)

UserController.java
@GetMapping
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids) {
        // 1.查询用户
//        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return userService.queryUserAndAddressByIds(ids);
    }
IUserService.java
public interface IUserService extends IService<User> {
    List<UserVO> queryUserAndAddressByIds(List<Long> ids);
}
UserServiceImpl.java
@Override
    public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
        // 1.查询用户
        List<User> users = listByIds(ids);
        if (CollUtil.isEmpty(users)) {
            return Collections.emptyList();
        }
        // 2.查询地址
        // 2.1 获取用户id集合
        List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
        // 2.2 根据用户id查询地址 这是全部地址
        List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();
        // 2.3 转换地址VO
        List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
        // 2.4 梳理地址集合分组处理,分类整理,相同用户放入一个集合(组)中
        Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);
        if (CollUtil.isNotEmpty(addressVOList)){
            addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
        }
        // 3.转换VO返回
        List<UserVO> list = new ArrayList<>(users.size());
        for (User user : users) {
            // 3.1 转换User的Po为VO
            UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
            list.add(userVO);

            // 3.2 转换地址VO
            userVO.setAddresses(addressMap.get(user.getId()));
        }
        return null;
    }

扩展功能—逻辑删除要在数据库里面创建一个deleted表

订单不进行真实删除,一旦采用逻辑删除其他都不能用,需要添加配置信息

逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为1
  • 查询时只查询标记为0的数据

例如逻辑删除字段为deleted:

• 删除操作:

# 是0才删除 是1就不用删除 所以用AND
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0

• 查询操作:

# 查询未删除的数据
SELECT * FROM user WHERE deleted = 0
逻辑删除

MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:

mybatis-plus: 
  global-config:    
   db-config:
    logic-delete-field: flag # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
    logic-delete-value: 1 # 逻辑已删除值(默认为 1)
    logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
逻辑删除本身也有自己的问题,比如:

• 会导致数据库表垃圾数据越来越多,影响查询效率
• SQL中全都需要对逻辑删除字段做判断,影响查询效率

因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

扩展功能—枚举处理器

像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举Integer转换,非常麻烦。

因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换

3.3.1.定义枚举

我们定义一个用户状态的枚举:

User.java //使用枚举类型
// 使用状态(1正常 2冻结)
private UserStatus status;

要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。 MybatisPlus提供了@EnumValue注解来标记枚举属性:

package com.itheima.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
    @EnumValue
    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

3.3.2.配置枚举处理器MP增加了Enum和JSON处理器

在application.yaml文件中添加配置:

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
UserVO.java
package com.itheima.mp.domain.vo;

import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {

    @ApiModelProperty("用户id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("详细信息")
    private String info;

    @ApiModelProperty("使用状态(1正常 2冻结)")
    private UserStatus status;

    @ApiModelProperty("账户余额")
    private Integer balance;

    @ApiModelProperty("用户的收货地址")
    private List<AddressVO> addresses;
}
UserServiceImpl.java
 @Override
    public UserVO queryUserAndAddressById(Long id) {
        // 1.查询用户
        User user = getById(id);
        if (user == null || user.getStatus() == UserStatus.FREEZE) {
            throw new RuntimeException("用户状态异常");
        }
        // 2.查询方法
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, id).list();
        // 3.封装VO
        // 3.1 转User的PO为VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        if (CollUtil.isEmpty(addresses)) {
           userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }
想要前端返回正常还是冻结 @JsonValue
package com.itheima.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
    @EnumValue
    private final int value;
    @JsonValue
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

扩展功能—JSON处理器AbstractJsonTypeHandler

数据库中user表中有一个json类型的字段

名称 数据类型 注释
info JSON 详细信息
。。。。。。。。。。。。

JSON:
{ “age”:20,
“intro”: “”青年”,
“gender”:”male”}

这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。

而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。

接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:

@TableField(typeHandler = JacksonTypeHandler.class) 定义类型处理器
@TableName(value = "user", autoResultMap = true)

User.java
@Data
@TableName(value = "user", autoResultMap = true)
public class User {

    /**
     * 用户id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 详细信息
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
}
UserVO.java
package com.itheima.mp.domain.vo;

import com.itheima.mp.domain.po.UserInfo;
import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {

    @ApiModelProperty("用户id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("详细信息")
    private UserInfo info;

    @ApiModelProperty("使用状态(1正常 2冻结)")
    private UserStatus status;

    @ApiModelProperty("账户余额")
    private Integer balance;

    @ApiModelProperty("用户的收货地址")
    private List<AddressVO> addresses;
}
package com.itheima.mp.domain.po;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor(staticName = "of")
public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
    // 添加静态方法 of
    public static UserInfo of(Integer age, String intro, String gender) {
        return new UserInfo(age, intro, gender);
    }
}
UserMapperTest.java
@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testInsert() {
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo(UserInfo.of(14, "英文老师", "female"));
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userMapper.insert(user);
    }

插件功能—分页插件基本用法

MyBatisPlus提供的内置拦截器有下面这些:

序号 拦截器 描述
1 TenantLineInnerInterceptor 多租户插件
2 DynamicTableNameInnerInterceptor 动态表名插件
3 PaginationInnerInterceptor 分页插件
4 OptimisticLockerInnerInterceptor 乐观锁插件
5 IllegalSQLInnerInterceptor SQL性能规范插件,检测并拦截垃圾SQL
6 BlockAttackInnerInterceptor 防止全表更新和删除的插件
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件:【总拦截器】
@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 1. 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 2. 添加分页插件
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        pageInterceptor.setMaxLimit(1000L); // 设置分页上限
        interceptor.addInnerInterceptor(pageInterceptor);
        return interceptor;
    }
}
接着,就可以使用分页的API了:[IService里面就有]
@Test
    void testPageQuery() {
        // 1. 查询
        int pageNo = 1, pageSize = 5;
        // 1.1. 分页参数
        Page<User> page = Page.of(pageNo, pageSize);
        // 1.2. 排序参数, 通过OrderItem来指定
        page.addOrder(new OrderItem("balance", true));
        // 先按balance排序 再按id排序
        page.addOrder(new OrderItem("id", true));
        // 1.3. 分页查询
        Page<User> p = userService.page(page);
        // 2. 总条数
        System.out.println("total = " + p.getTotal());
        // 3. 总页数
        System.out.println("pages = " + p.getPages());
        // 4. 分页数据
        List<User> records = p.getRecords();
        records.forEach(System.out::println);
    }

插件功能—通用分页实体

遵循下面的接口规范,编写一个UserController接口,实现User的分页查询
参数 说明
请求方式 GET
请求路径 /users/page
请求参数 “pageNo”: 1
“pageSize”: 5
“sortBy”: “balance”
“isAsc”: false
“name”: “jack”
“status”: 1
返回值 “total”: 1005
“pages”: 201
“list”: 包含两个元素的数组,每个元素都是一个对象,包含以下键值对:
“id”: 1 或 2
“username”: “Jack” 或 “Rose”
“info”: 包含以下键值对的对象:
“age”: 21 或 20
“gender”: “male” 或 “female”
“intro”: “佛系青年” 或 “文艺青年”
“status”: “正常” 或 “冻结”
“balance”: 2000 或 1000
特殊说明 如果排序字段为空,默认按照更新时间排序 •排序字段不为空,则按照排序字段排序
准备一下请求参数和实体【封装成xxxQuery,若只返回前端则VO,给其他使用则DTO】

写一个 【统一的分页条件】 和 【统一的分页结果】

com/itheima/mp/domain/query/UserQuery.java
// 要记得继承哦
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery extends PageQuery{
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
com/itheima/mp/domain/query/PageQuery.java
package com.itheima.mp.domain.query;

import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("页码个数")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;

    public <T>  Page<T> toMpPage(OrderItem ... orders){
        // 1.分页条件
        Page<T> p = Page.of(pageNo, pageSize);
        // 2.排序条件
        // 2.1.先看前端有没有传排序字段
        if (sortBy != null) {
            p.addOrder(new OrderItem(sortBy, isAsc));
            return p;
        }
        // 2.2.再看有没有手动指定排序字段
        if(orders != null){
            p.addOrder(orders);
        }
        return p;
    }

    public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
        return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
    }

    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return toMpPage("create_time", false);
    }

    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return toMpPage("update_time", false);
    }
}
com/itheima/mp/domain/dto/PageDTO.java
package com.itheima.mp.domain.dto;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
@ApiModel(description = "分页结果")
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    private List<V> list;

    /**
     * 返回空分页结果
     * @param p MybatisPlus的分页结果
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> empty(Page<P> p){
        return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果
     * @param p MybatisPlus的分页结果
     * @param voClass 目标VO类型的字节码
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = BeanUtil.copyToList(records, voClass);
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
     * @param p MybatisPlus的分页结果
     * @param convertor PO到VO的转换函数
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }
}

com/itheima/mp/controller/UserController.java
@ApiOperation("根据复杂条件查询用户接口")
    @GetMapping("/list")
    public List<UserVO> queryUsers(UserQuery query) {
        return (List<UserVO>) userService.queryUsersPage(query);
    }
// 如果你想在字符串中表示一个大于号,
你可以直接输入 >,或者使用HTML实体 &gt; 
小于号可以使用 < 或者 &lt;,
等于号可以使用 = 或者 &equals

插件功能—通用分页实体与MP转换

需求:

  • 在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
  • 在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
最好直接封装通用部分
封装查询
 @Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        String name = query.getName();
        Integer status = query.getStatus();
        // 1.构建查询条件
        // 1.1 分页条件
        Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
        // 1.2 排序条件
        if (StrUtil.isNotBlank(query.getSortBy())) {
            // 不为空
            page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
        }else {
            // 为空,默认按照更新时间排序
            page.addOrder(new OrderItem("update_time", false));
        }
com/itheima/mp/domain/query/PageQuery.java
package com.itheima.mp.domain.query;

import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("页码个数")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;

    public <T>  Page<T> toMpPage(OrderItem ... orders){
        // 1.分页条件
        Page<T> p = Page.of(pageNo, pageSize);
        // 2.排序条件
        // 2.1.先看前端有没有传排序字段
        if (sortBy != null) {
            p.addOrder(new OrderItem(sortBy, isAsc));
            return p;
        }
        // 2.2.再看有没有手动指定排序字段
        if(orders != null){
            p.addOrder(orders);
        }
        return p;
    }

    public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
        return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
    }

    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return toMpPage("create_time", false);
    }

    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return toMpPage("update_time", false);
    }
}
封装结果
// 3.封装VO结果
        PageDTO<UserVO> dto = new PageDTO<>();
        // 3.1 总条数
        dto.setTotal(p.getTotal());
        // 3.2 总页数
        dto.setPages(p.getPages());
        // 3.3 当前页数据
        List<User> records = p.getRecords();
        if (CollUtil.isEmpty(records)) {
            dto.setList(Collections.emptyList());
            return dto;
        }
        // 3.4 拷贝user的VO
        dto.setList(BeanUtil.copyToList(records, UserVO.class));
        // 4.返回
        return dto;
    }
com/itheima/mp/domain/dto/PageDTO.java
package com.itheima.mp.domain.dto;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
@ApiModel(description = "分页结果")
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    private List<V> list;

    /**
     * 返回空分页结果
     * @param p MybatisPlus的分页结果
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> empty(Page<P> p){
        return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果
     * @param p MybatisPlus的分页结果
     * @param voClass 目标VO类型的字节码
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = BeanUtil.copyToList(records, voClass);
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
     * @param p MybatisPlus的分页结果
     * @param convertor PO到VO的转换函数
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }
}
UserServiceImpl.java
@Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        String name = query.getName();
        Integer status = query.getStatus();
        // 1.构建查询条件
        // 1.1 分页条件
        Page<User> page = query.toMpPageDefaultSortByUpdateTimeDesc();
        // 2. 分页查询
        Page<User> p = lambdaQuery()
                .like(name != null, User::getUsername, name)
                .eq(status != null, User::getStatus, status)
                .page(page);
        // 3. 封装VO结果
//        return PageDTO.of(p, UserVO.class); 属性转换 ↓
        return PageDTO.of(p, user -> {
            // 1.拷贝基础属性
            UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
            // 2.处理特殊逻辑 密码加**
            vo.setUsername(vo.getUsername().substring(0, vo.getUsername().length()-2)+"**");
            return vo;
        });
    }



tilas-all 成功案例

package com.itheima.domain.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GenderStatisticsDTO {
    private String name;
    private Integer value;

}
==================================================
package com.itheima.domain.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class JobStatisticsDTO {
    private List<String> jobList;
    private List<Long> dataList;
}
package com.itheima.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.domain.dto.CombinedStatisticsDTO;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.pojo.Emp;

import java.util.List;

public interface ReportService extends IService<Emp> {
    List<GenderStatisticsDTO> getGenderStatistics(); // 获取性别统计信息
    JobStatisticsDTO getJobStatistics(); // 获取职位统计信息
}


//    CombinedStatisticsDTO getCombinedStatistics();
com/itheima/controller/ReportController.java
package com.itheima.controller;

import com.itheima.domain.dto.CombinedStatisticsDTO;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.pojo.Result;
import com.itheima.service.ReportService;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Api(tags = "报表统计接口")
@RestController
@RequestMapping("/report")
@RequiredArgsConstructor
public class ReportController {

    private final ReportService reportService;

    // 获取合并后的统计信息
//    @GetMapping("/statistics")
//    public Result<CombinedStatisticsDTO> getStatistics() {
//        CombinedStatisticsDTO combinedStatistics = reportService.getCombinedStatistics();
//        return Result.success(combinedStatistics);
//    }

    // 获取员工性别统计信息
    @GetMapping("/empGenderData")
    public Result<List<GenderStatisticsDTO>> getEmployeeGenderStatistics() {
        List<GenderStatisticsDTO> genderStatistics = reportService.getGenderStatistics();
        return Result.success(genderStatistics);
    }

    // 获取员工职位统计信息
    @GetMapping("/empJobData")
    public Result<JobStatisticsDTO> getEmployeeJobStatistics() {
        JobStatisticsDTO jobStatistics = reportService.getJobStatistics();
        return Result.success(jobStatistics);
    }
}
com/itheima/service/impl/ReportServiceImpl.java
package com.itheima.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.mapper.ReportMapper;
import com.itheima.pojo.Emp;
import com.itheima.service.ReportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class ReportServiceImpl extends ServiceImpl<ReportMapper, Emp> implements ReportService {

    private final ReportMapper reportMapper;

    @Autowired
    public ReportServiceImpl(ReportMapper reportMapper) {
        this.reportMapper = reportMapper;
    }

    // 获取员工职位统计信息
    @Override
    public JobStatisticsDTO getJobStatistics() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();

        // 修改查询字段,注意这里你仍然需要写正确的字段名
        wrapper.select("CASE job WHEN 1 THEN '班主任' WHEN 2 THEN '讲师' WHEN 3 THEN '学工主管' WHEN 4 THEN '校研主管' WHEN 5 THEN '咨询师' ELSE '其他' END AS 职位",
                        "COUNT(*) AS 数量")
                .groupBy("job");

        // 通过 selectMaps 执行查询
        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 创建两个列表来存储职位和数量
        List<String> jobTitles = new ArrayList<>();
        List<Long> counts = new ArrayList<>();

        // 遍历查询结果并填充列表
        for (Map<String, Object> stat : statistics) {
            jobTitles.add((String) stat.get("职位"));
            counts.add((Long) stat.get("数量"));
        }

        // 返回JobStatisticsDTO对象,传入两个列表
        return new JobStatisticsDTO(jobTitles, counts);
    }

    // 获取员工性别统计信息
    @Override
    public List<GenderStatisticsDTO> getGenderStatistics() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select("gender", "COUNT(gender) AS value")
                .groupBy("gender");

        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 转换为 GenderStatisticsDTO
        return statistics.stream()
                .map(stat -> {
                    String genderName = "1".equals(String.valueOf(stat.get("gender"))) ? "男性员工" : "女性员工";
                    int count = ((Number) stat.get("value")).intValue();
                    return new GenderStatisticsDTO(genderName, count);
                })
                .collect(Collectors.toList());
    }
}
上面的获取员工职位属性已修改为高级版本
// 获取员工职位统计信息
    @Override
    public JobStatisticsDTO getJobStatistics() {
        // 1. 构建查询条件
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select("job", "COUNT(*) AS count")
                .groupBy("job");

        // 2. 查询数据
        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 如果返回结果为 null 或为空列表,返回默认对象
        if (statistics == null || statistics.isEmpty()) {
            return new JobStatisticsDTO(new ArrayList<>(), new ArrayList<>());
        }

        // 3. 转换结果:处理 `null` 值和字段映射
        List<String> jobTitles = new ArrayList<>();
        List<Long> counts = new ArrayList<>();

        for (Map<String, Object> stat : statistics) {
            if (stat == null) {
                continue; // 跳过 null 数据
            }

            // 使用 `getOrDefault` 方法,确保不会返回 null
            Integer jobCode = (Integer) stat.getOrDefault("job", -1);
            Long count = stat.get("count") == null ? 0L : ((Number) stat.get("count")).longValue();

            // 如果 jobCode 是 -1 或其他无效值,则视为“其他”
            String jobTitle = switch (jobCode) {
                case 1 -> "班主任";
                case 2 -> "讲师";
                case 3 -> "学工主管";
                case 4 -> "校研主管";
                case 5 -> "咨询师";
                default -> "其他";
            };

            jobTitles.add(jobTitle);
            counts.add(count);
        }

        // 返回封装好的 DTO 对象
        return new JobStatisticsDTO(jobTitles, counts);
    }
阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.